Compare commits

..

46 Commits

Author SHA1 Message Date
J. Nick Koston b2257caeb7 touch ups 2026-05-22 09:11:01 -05:00
J. Nick Koston 0ec0ea30ac single pass 2026-05-22 09:06:15 -05:00
J. Nick Koston 584b32c8b3 address copilot, cleanups 2026-05-22 09:01:39 -05:00
J. Nick Koston 4033a8b83a Apply suggestions from code review
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-05-22 08:53:47 -05:00
J. Nick Koston add8a5f799 Merge branch 'dev' into cache-split-tests 2026-05-22 08:53:27 -05:00
DeerMaximum 40c0d79d1d Replaced duplicate constant with homeassistant.const in NINA (#171852) 2026-05-22 15:41:04 +02:00
J. Nick Koston 7c137b5c73 cleanup 2026-05-22 08:34:32 -05:00
Franck Nijhof bef8632d78 Fix OpenHome config flow crash when UDN is a list (#171841) 2026-05-22 15:23:42 +02:00
Duco Sebel f00decfaa3 Use uptime device class for HomeWizard uptime sensor (#171830) 2026-05-22 15:23:09 +02:00
Manu 42e7add026 Add selector options translations to System Bridge integration (#171771) 2026-05-22 15:22:22 +02:00
Franck Nijhof 263aa3f16e Fix Hue device trigger crash for devices removed from bridge (#171844) 2026-05-22 15:18:00 +02:00
mhuiskes 03b364dcf0 Refactor zeversolar tests: use fixtures, patch at use site, add unique_id (#171697) 2026-05-22 14:58:56 +02:00
Duco Sebel 3b1aaf39af Bumb python homewizard energy 10.1.0 (#171826) 2026-05-22 14:51:58 +02:00
J. Nick Koston 4a6c5b5a22 cleanups 2026-05-22 07:46:31 -05:00
Franck Nijhof b82ba43fa4 Add pylint checker for invalid MDI icon references (#171824)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 13:45:40 +02:00
starkillerOG d81ef5593c Bump reolink_aio to 0.20.0: Reolink battery camera support (#171836) 2026-05-22 12:59:33 +02:00
Manu 5c5e50f024 Fix platform unloading in System Bridge integration (#171822) 2026-05-22 12:56:03 +02:00
Lex Postma e796d9c467 Update strings.json to align with HomeWizard app (#171740)
Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com>
2026-05-22 12:54:58 +02:00
Karl Beecken 342f23526f Remove empty requirements_test_all.txt (#171530 follow-up) (#171834) 2026-05-22 12:38:58 +02:00
Erik Montnemery 814ec697cf Remove advanced mode from hue service actions (#171442) 2026-05-22 11:45:33 +02:00
Erik Montnemery 120f1446d4 Rename advanced section to additional options in telegram_bot service actions (#171460) 2026-05-22 11:44:05 +02:00
Franck Nijhof 170af75b7d Fix Lutron Caseta battery sensor crash on unsupported devices (#171829) 2026-05-22 11:37:05 +02:00
Ariel Ebersberger 5432d29489 Use is/is not for same-enum identity comparisons (tests) (#171689) 2026-05-22 11:32:27 +02:00
Franck Nijhof 8098f4f6bc Fix invalid MDI icon references (#171831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 11:25:28 +02:00
Simone Chemelli 6a70077687 Fix exception translation placeholder mismatches in comelit (#171748) 2026-05-22 11:17:17 +02:00
Max Michels 5dbb0464ba Replace duplicate constants with homeassistant.const imports (#171815) 2026-05-22 11:10:21 +02:00
dependabot[bot] 1df165ea02 Bump j178/prek-action from 2.0.3 to 2.0.4 (#171812)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 10:39:43 +02:00
Manu 62542eb911 Replace duplicate constants with homeassistant.const imports in xiaomi_miio (#171823) 2026-05-22 10:39:09 +02:00
Max Michels a842cac34c Replace duplicate constants with homeassistant.const imports (#171817) 2026-05-22 10:38:06 +02:00
Simone Chemelli 2460f688e3 Add missing exception translation keys in alexa_devices (#171749) 2026-05-22 10:34:00 +02:00
Simone Chemelli a868ea443c Fix hardcoded exception strings in uptimerobot (#171744) 2026-05-22 10:33:07 +02:00
Franck Nijhof 1d8565483b Apply web search citation stripping for GPT-5.x models in OpenAI conversation (#170956) 2026-05-22 10:31:10 +02:00
dependabot[bot] 1ef3301253 Bump github/codeql-action from 4.35.4 to 4.35.5 (#171813)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-22 09:47:16 +02:00
Manu 525952f016 Add entity translations to System Bridge integration (#171807) 2026-05-22 09:00:54 +02:00
Shay Levy 3257275c5a Fix LG webOS TV hardcoded exception strings (#171777) 2026-05-22 08:28:19 +02:00
Max Michels cb54fd4921 Replace duplicate constants with homeassistant.const imports (#171809) 2026-05-22 07:57:08 +02:00
Max Michels b391fc61ea Replace duplicate constants with homeassistant.const imports (#171808) 2026-05-22 07:56:29 +02:00
J. Nick Koston 1009ce4180 Merge branch 'dev' into cache-split-tests 2026-05-21 23:09:44 -05:00
J. Nick Koston 22fb68b7a1 Revert "DNM: test cache, touch cloud manifest only"
This reverts commit a8bc244a7a.
2026-05-21 16:53:19 -05:00
J. Nick Koston 81e06539e6 Revert "DNM: test cache bust, touch cloud conftest"
This reverts commit 7c18b67b2e.
2026-05-21 16:53:19 -05:00
J. Nick Koston 7c18b67b2e DNM: test cache bust, touch cloud conftest 2026-05-21 16:48:05 -05:00
J. Nick Koston a8bc244a7a DNM: test cache, touch cloud manifest only 2026-05-21 16:44:43 -05:00
J. Nick Koston 5975f4b179 Skip cache walking when --cache is not passed
Address Copilot review feedback on the cache PR:

* Split collect_tests into _collect_tests_uncached (the original
  directory-based pre-cache flow) and _collect_tests_cached (walks
  the tree to build per-file hashes).  Without --cache we now skip
  the walk + per-file hash entirely.
* A single-file root has no ancestor conftests to walk, so the
  conftest_hash would always be empty and stale counts could survive
  a real conftest change; bypass the cache for the file-root case.
* Save the cache file with explicit utf-8 encoding and
  ensure_ascii=False.
2026-05-21 16:08:44 -05:00
J. Nick Koston 9ed16b63a3 Cache per-file test counts in split_tests
Persist the result of pytest --collect-only between CI runs as a JSON
file keyed by content hash, so unchanged test files are served from
cache and only edited or new files are re-collected.  The cache is
self-healing:

* Missing, corrupt, or wrong-version files fall back to a full collect.
* Any conftest.py change anywhere under the test root invalidates the
  whole cache, so fixture parametrization shifts cannot silently skew
  counts.
* Files pytest returns nothing for (helper modules named test_*.py with
  no test functions) are cached as zero so they don't get re-collected
  forever.

Walking is done once with os.walk (~2x faster than Path.rglob) and
collects test files plus conftests in a single pass.  When the cache
is fully cold we feed pytest top-level directories rather than
thousands of file paths so cold runs stay as fast as before the cache
landed.

Wire the new --cache flag through the prepare-pytest-full job and back
the cache file with actions/cache so PRs can pick up the latest dev
snapshot via restore-keys.  Local timings: cold 11s, warm with no diff
0.4s, warm with one file edited 2.3s.
2026-05-21 15:56:08 -05:00
J. Nick Koston 8dadaa2f9e Filter fan-out children and fail fast on empty batch list
Only pass directories and test_*.py files to pytest --collect-only so
helpers like tests/components/conftest.py and tests/components/common.py
are not treated as explicit collection targets, and bail out with a
clear error if no eligible paths are found instead of running pytest
with no arguments.
2026-05-21 15:17:42 -05:00
J. Nick Koston 4f98c71586 Run pytest --collect-only in parallel batches in split_tests
cProfile showed 99.6% of split_tests.py wall time was spent in the
single pytest --collect-only subprocess.  Fan out the collection across
``os.cpu_count()`` workers; round-robin chunking keeps each batch
roughly equal, and tests/components is expanded one level deeper so
the ~1000 integration subdirectories distribute evenly.  Local wall
time dropped from ~132s to ~11s on an 18-core box.  Bucket output is
unchanged because we still parse the same pytest -qq output, just
aggregated from multiple invocations.
2026-05-21 15:10:01 -05:00
199 changed files with 12563 additions and 1324 deletions
+1
View File
@@ -15,6 +15,7 @@ Dockerfile.dev linguist-language=Dockerfile
# Generated files
CODEOWNERS linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
+29 -3
View File
@@ -281,7 +281,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
RUFF_OUTPUT_FORMAT: github
@@ -302,7 +302,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
with:
extra-args: --all-files zizmor
@@ -917,12 +917,38 @@ jobs:
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore pytest test counts cache
id: cache-pytest-counts
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: pytest_test_counts.json
key: >-
pytest-counts-${{ runner.os }}-${{ runner.arch }}-${{
steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}-${{ github.sha }}
restore-keys: |
pytest-counts-${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }}-
- name: Run split_tests.py
env:
TEST_GROUP_COUNT: ${{ needs.info.outputs.test_group_count }}
run: |
. venv/bin/activate
python -m script.split_tests ${TEST_GROUP_COUNT} tests
python -m script.split_tests \
--cache pytest_test_counts.json \
${TEST_GROUP_COUNT} tests
- name: Save pytest test counts cache
# Only the canonical dev push writes the cache, otherwise every PR
# build would create a new entry and the actions/cache quota fills
# up with near-duplicate snapshots. PRs and feature branches still
# restore from dev's most recent cache via restore-keys.
if: |
github.event_name == 'push'
&& github.ref == 'refs/heads/dev'
&& steps.cache-pytest-counts.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: pytest_test_counts.json
key: ${{ steps.cache-pytest-counts.outputs.cache-primary-key }}
- name: Upload pytest_buckets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: "/language:python"
@@ -11,7 +11,7 @@
"service": "mdi:dialpad"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
"service": "mdi:bell-ring"
}
}
}
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
translation_placeholders={"error": repr(err)},
) from err
except CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
@@ -102,6 +102,9 @@
"entry_not_loaded": {
"message": "Entry not loaded: {entry}"
},
"invalid_auth": {
"message": "Invalid authentication credentials: {error}"
},
"invalid_device_id": {
"message": "Invalid device ID specified: {device_id}"
},
+5 -13
View File
@@ -130,25 +130,17 @@ def async_register_callback(
callback: BluetoothCallback,
match_dict: BluetoothCallbackMatcher | None,
mode: BluetoothScanningMode,
*,
scan_interval: float | None = None,
scan_duration: float | None = None,
) -> Callable[[], None]:
"""Register to receive a callback on bluetooth change.
When ``mode`` is ACTIVE and ``scan_interval`` is provided, the
matched address (taken from ``match_dict["address"]``) is registered
with habluetooth's active-scan scheduler so AUTO-mode scanners flip
on demand every ``scan_interval`` seconds for ``scan_duration``
seconds, instead of forcing continuous active scanning. Without an
address in the matcher the active-scan request is skipped; the
callback itself still fires normally.
mode is currently not used as we only support active scanning.
Passive scanning will be available in the future. The flag
is required to be present to avoid a future breaking change
when we support passive scanning.
Returns a callback that can be used to cancel the registration.
"""
return _get_manager(hass).async_register_callback(
callback, match_dict, mode, scan_interval, scan_duration
)
return _get_manager(hass).async_register_callback(callback, match_dict)
async def async_process_advertisements(
+1 -20
View File
@@ -202,9 +202,6 @@ class HomeAssistantBluetoothManager(BluetoothManager):
self,
callback: BluetoothCallback,
matcher: BluetoothCallbackMatcher | None,
mode: BluetoothScanningMode = BluetoothScanningMode.ACTIVE,
scan_interval: float | None = None,
scan_duration: float | None = None,
) -> Callable[[], None]:
"""Register a callback."""
callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback)
@@ -219,31 +216,15 @@ class HomeAssistantBluetoothManager(BluetoothManager):
connectable = callback_matcher[CONNECTABLE]
self._callback_index.add_callback_matcher(callback_matcher)
# If the caller declared an active-scan cadence and the matcher
# targets a specific address, also wire it into habluetooth's
# active-scan scheduler so AUTO-mode scanners flip active on
# demand for this device instead of forcing continuous scanning.
cancel_active_scan: Callable[[], None] | None = None
if (
scan_interval is not None
and mode is not BluetoothScanningMode.PASSIVE
and (address := callback_matcher.get(ADDRESS)) is not None
):
cancel_active_scan = self.async_register_active_scan(
address, scan_interval, scan_duration
)
def _async_remove_callback() -> None:
self._callback_index.remove_callback_matcher(callback_matcher)
if cancel_active_scan is not None:
cancel_active_scan()
# If we have history for the subscriber, we can trigger the callback
# immediately with the last packet so the subscriber can see the
# device.
history = self._connectable_history if connectable else self._all_history
service_infos: Iterable[BluetoothServiceInfoBleak] = []
if (address := callback_matcher.get(ADDRESS)) is not None:
if address := callback_matcher.get(ADDRESS):
if service_info := history.get(address):
service_infos = [service_info]
else:
@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.3",
"bluetooth-data-tools==1.29.11",
"dbus-fast==5.0.3",
"habluetooth==6.3.0"
"habluetooth==6.2.0"
]
}
@@ -65,11 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
translation_placeholders={"error": repr(err)},
) from err
except aiocomelit_exceptions.CannotAuthenticate as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise InvalidAuth(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
finally:
await api.logout()
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
"""Implementing the Currencylayer sensor."""
_attr_attribution = "Data provided by currencylayer.com"
_attr_icon = "mdi:currency"
_attr_icon = "mdi:currency-usd"
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
"""Initialize the sensor."""
+1 -1
View File
@@ -23,7 +23,7 @@
"service": "mdi:refresh"
},
"set_dhw_override": {
"service": "mdi:water-heater"
"service": "mdi:water-boiler"
},
"set_system_mode": {
"service": "mdi:pencil"
+1 -1
View File
@@ -16,7 +16,7 @@ class DeviceType(Enum):
GAME_CONSOLE = "mdi:nintendo-game-boy"
STREAMING_DONGLE = "mdi:cast"
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
DISC_PLAYER = "mdi:disk-player"
DISC_PLAYER = "mdi:disc-player"
REMOTE_CONTROL = "mdi:remote-tv"
RADIO = "mdi:radio"
PHOTO_CAMERA = PHOTOS = "mdi:camera"
+1 -1
View File
@@ -2,7 +2,7 @@
"entity": {
"button": {
"sync_clock": {
"default": "mdi:clock-sync"
"default": "mdi:clock-check"
}
},
"number": {
-2
View File
@@ -2,8 +2,6 @@
from homeassistant.const import Platform
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
ATTR_TIME_PERIOD = "time_period"
ATTR_ONOFF = "on_off"
CONF_CODE = "2fa"
+1 -1
View File
@@ -12,12 +12,12 @@ from homeassistant.components.light import (
ColorMode,
LightEntity,
)
from homeassistant.const import ATTR_MODE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from . import HiveConfigEntry, refresh_system
from .const import ATTR_MODE
from .entity import HiveEntity
PARALLEL_UPDATES = 0
+1 -2
View File
@@ -6,12 +6,11 @@ from typing import Any
from apyhiveapi import Hive
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.const import ATTR_MODE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HiveConfigEntry, refresh_system
from .const import ATTR_MODE
from .entity import HiveEntity
PARALLEL_UPDATES = 0
@@ -13,6 +13,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==10.0.1"],
"requirements": ["python-homewizard-energy==10.1.0"],
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
}
+2 -10
View File
@@ -35,7 +35,6 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .const import DOMAIN
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
@@ -66,13 +65,6 @@ def to_percentage(value: float | None) -> float | None:
return value * 100 if value is not None else None
def uptime_to_datetime(value: int) -> datetime:
"""Convert seconds to datetime timestamp."""
return utcnow().replace(microsecond=0) - timedelta(seconds=value)
uptime_to_stable_datetime = ignore_variance(uptime_to_datetime, timedelta(minutes=5))
SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
HomeWizardSensorEntityDescription(
key="smr_version",
@@ -643,7 +635,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
HomeWizardSensorEntityDescription(
key="uptime",
translation_key="uptime",
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
has_fn=(
@@ -651,7 +643,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
),
value_fn=(
lambda data: (
uptime_to_stable_datetime(data.system.uptime_s)
utcnow() - timedelta(seconds=data.system.uptime_s)
if data.system is not None and data.system.uptime_s is not None
else None
)
@@ -61,13 +61,14 @@
},
"select": {
"battery_group_mode": {
"name": "Battery group mode",
"name": "Battery group charging strategy",
"state": {
"predictive": "Smart charging",
"standby": "Standby",
"to_full": "Manual charge mode",
"zero": "Zero mode",
"zero_charge_only": "Zero mode (charge only)",
"zero_discharge_only": "Zero mode (discharge only)"
"to_full": "One-time full charge",
"zero": "Net zero",
"zero_charge_only": "Net zero (charge only)",
"zero_discharge_only": "Net zero (discharge only)"
}
}
},
+13 -12
View File
@@ -31,15 +31,16 @@ activate_scene:
dynamic:
selector:
boolean:
speed:
advanced: true
selector:
number:
min: 0
max: 100
brightness:
advanced: true
selector:
number:
min: 1
max: 255
scene_customization:
collapsed: true
fields:
speed:
selector:
number:
min: 0
max: 100
brightness:
selector:
number:
min: 1
max: 255
+6 -1
View File
@@ -184,7 +184,12 @@
"name": "Transition"
}
},
"name": "Activate Hue scene"
"name": "Activate Hue scene",
"sections": {
"scene_customization": {
"name": "Scene customization"
}
}
},
"hue_activate_scene": {
"description": "Activates a Hue scene stored in the Hue hub.",
@@ -87,6 +87,8 @@ def async_get_triggers(
# Get Hue device id from device identifier
hue_dev_id = get_hue_device_id(device_entry)
if hue_dev_id is None or hue_dev_id not in api.devices:
return []
# extract triggers from all button resources of this Hue device
triggers: list[dict[str, Any]] = []
model_id = api.devices[hue_dev_id].product_data.product_name
@@ -1,7 +1,7 @@
{
"entity": {
"binary_sensor": {
"erev_shabbat_hag": { "default": "mdi:candle-light" },
"erev_shabbat_hag": { "default": "mdi:candle" },
"issur_melacha_in_effect": { "default": "mdi:power-plug-off" },
"motzei_shabbat_hag": { "default": "mdi:fire" }
},
+1 -1
View File
@@ -7,7 +7,7 @@
"service": "mdi:lock-open"
},
"disable": {
"service": "mdi:fash-off"
"service": "mdi:flash-off"
},
"enable": {
"service": "mdi:flash"
+4 -4
View File
@@ -28,25 +28,25 @@
"ice_maker": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_bottom_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_middle_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_top_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
}
},
@@ -3,7 +3,7 @@
from datetime import timedelta
from typing import Any
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -131,7 +131,11 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
async def async_update(self) -> None:
"""Fetch the latest battery status from the bridge."""
status = await self._smartbridge.get_battery_status(self.device_id)
try:
status = await self._smartbridge.get_battery_status(self.device_id)
except BridgeResponseError:
self._attr_is_on = None
return
normalized_status = status.strip().casefold() if status else None
if normalized_status == BATTERY_STATUS_LOW:
self._attr_is_on = True
+1 -1
View File
@@ -102,7 +102,7 @@
"default": "mdi:home-lightning-bolt"
},
"eve_weather_trend": {
"default": "mdi:weather",
"default": "mdi:weather-cloudy",
"state": {
"cloudy": "mdi:weather-cloudy",
"rainy": "mdi:weather-rainy",
@@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import ATTR_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -14,7 +15,6 @@ from .const import (
ATTR_DESCRIPTION,
ATTR_EXPIRES,
ATTR_HEADLINE,
ATTR_ID,
ATTR_RECOMMENDED_ACTIONS,
ATTR_SENDER,
ATTR_SENT,
-2
View File
@@ -29,8 +29,6 @@ ATTR_SEVERITY: str = "severity"
ATTR_RECOMMENDED_ACTIONS: str = "recommended_actions"
ATTR_AFFECTED_AREAS: str = "affected_areas"
ATTR_WEB: str = "web"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_ID: str = "id"
ATTR_SENT: str = "sent"
ATTR_START: str = "start"
ATTR_EXPIRES: str = "expires"
@@ -595,8 +595,8 @@ class OpenAIBaseLLMEntity(Entity):
)
)
if "reasoning" not in model_args:
# Reasoning models handle this correctly with just a prompt
if not model_args["model"].startswith("o"):
# o-series models handle this correctly with just a prompt
remove_citations = True
tools.append(web_search)
@@ -37,11 +37,15 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("async_step_ssdp: Incomplete discovery, ignoring")
return self.async_abort(reason="incomplete_discovery")
_LOGGER.debug(
"async_step_ssdp: setting unique id %s", discovery_info.upnp[ATTR_UPNP_UDN]
)
udn = discovery_info.upnp[ATTR_UPNP_UDN]
if isinstance(udn, list):
if not udn:
return self.async_abort(reason="incomplete_discovery")
udn = udn[0]
await self.async_set_unique_id(discovery_info.upnp[ATTR_UPNP_UDN])
_LOGGER.debug("async_step_ssdp: setting unique id %s", udn)
await self.async_set_unique_id(udn)
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ssdp_location})
_LOGGER.debug(
+16 -16
View File
@@ -29,29 +29,29 @@
}
},
"sensor": {
"translation_key_0": {
"default": "mdi:abc"
"flow_sensor_clicks_cubic_meter": {
"default": "mdi:water-pump"
},
"translation_key_1": {
"default": "mdi:abc"
"flow_sensor_consumed_liters": {
"default": "mdi:water-pump"
},
"translation_key_2": {
"default": "mdi:abc"
"flow_sensor_leak_clicks": {
"default": "mdi:pipe-leak"
},
"translation_key_3": {
"default": "mdi:abc"
"flow_sensor_leak_volume": {
"default": "mdi:pipe-leak"
},
"translation_key_4": {
"default": "mdi:abc"
"flow_sensor_start_index": {
"default": "mdi:water-pump"
},
"translation_key_5": {
"default": "mdi:abc"
"flow_sensor_watering_clicks": {
"default": "mdi:water-pump"
},
"translation_key_6": {
"default": "mdi:abc"
"last_leak_detected": {
"default": "mdi:pipe-leak"
},
"translation_key_7": {
"default": "mdi:abc"
"rain_sensor_rain_start": {
"default": "mdi:weather-pouring"
}
},
"switch": {
@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.19.1"]
"requirements": ["reolink-aio==0.20.0"]
}
@@ -23,7 +23,7 @@ from homeassistant.config_entries import (
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_USERNAME
from homeassistant.const import CONF_REGION, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -38,7 +38,6 @@ from . import RoborockConfigEntry
from .const import (
CONF_BASE_URL,
CONF_ENTRY_CODE,
CONF_REGION,
CONF_SHOW_BACKGROUND,
CONF_SHOW_ROOMS,
CONF_SHOW_WALLS,
@@ -13,8 +13,6 @@ CONF_USER_DATA = "user_data"
CONF_SHOW_BACKGROUND = "show_background"
CONF_SHOW_WALLS = "show_walls"
CONF_SHOW_ROOMS = "show_rooms"
# pylint: disable-next=home-assistant-duplicate-const
CONF_REGION = "region"
REGION_OPTIONS = ["auto", "us", "eu", "ru", "cn"]
# Option Flow steps
+1 -2
View File
@@ -15,6 +15,7 @@ from homeassistant.components.sensor import (
SensorEntity,
)
from homeassistant.const import (
ATTR_MODEL,
CONF_MAC,
CONF_NAME,
EVENT_HOMEASSISTANT_STOP,
@@ -30,8 +31,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
ATTR_DEVICE = "device"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
BLE_TEMP_HANDLE = 0x24
BLE_TEMP_UUID = "0000ff92-0000-1000-8000-00805f9b34fb"
-4
View File
@@ -38,12 +38,8 @@ PLATFORMS = [
Platform.SENSOR,
]
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_LOCK = "lock"
SERVICE_REMOTE_START = "remote_start"
SERVICE_REMOTE_STOP = "remote_stop"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_UNLOCK = "unlock"
SERVICE_UNLOCK_SPECIFIC_DOOR = "unlock_specific_door"
ATTR_DOOR = "door"
@@ -4,9 +4,10 @@ import logging
from subarulink.exceptions import SubaruException
from homeassistant.const import SERVICE_UNLOCK
from homeassistant.exceptions import HomeAssistantError
from .const import SERVICE_REMOTE_START, SERVICE_UNLOCK, VEHICLE_NAME, VEHICLE_VIN
from .const import SERVICE_REMOTE_START, VEHICLE_NAME, VEHICLE_VIN
_LOGGER = logging.getLogger(__name__)
@@ -454,9 +454,7 @@ async def async_unload_entry(
hass: HomeAssistant, entry: SystemBridgeConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator = entry.runtime_data
@@ -17,15 +17,51 @@
"boot_time": {
"default": "mdi:av-timer"
},
"cpu_power_core": {
"default": "mdi:chip"
},
"cpu_power_package": {
"default": "mdi:chip"
},
"cpu_speed": {
"default": "mdi:speedometer"
},
"display_refresh_rate": {
"default": "mdi:monitor"
},
"display_resolution_x": {
"default": "mdi:monitor"
},
"display_resolution_y": {
"default": "mdi:monitor"
},
"displays_connected": {
"default": "mdi:monitor"
},
"gpu_core_clock_speed": {
"default": "mdi:speedometer"
},
"gpu_fan_speed": {
"default": "mdi:fan"
},
"gpu_memory_clock_speed": {
"default": "mdi:speedometer"
},
"gpu_memory_free": {
"default": "mdi:memory"
},
"gpu_memory_used": {
"default": "mdi:memory"
},
"gpu_memory_used_percentage": {
"default": "mdi:memory"
},
"gpu_power_usage": {
"default": "mdi:lightning-bolt"
},
"gpu_usage_percentage": {
"default": "mdi:percent"
},
"kernel": {
"default": "mdi:devices"
},
@@ -38,6 +74,9 @@
"memory_used": {
"default": "mdi:memory"
},
"memory_used_percentage": {
"default": "mdi:memory"
},
"os": {
"default": "mdi:devices"
},
@@ -47,6 +86,12 @@
"processes": {
"default": "mdi:counter"
},
"processes_load_cpu": {
"default": "mdi:percent"
},
"space_used": {
"default": "mdi:harddisk"
},
"version": {
"default": "mdi:counter"
},
@@ -27,7 +27,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED, StateType
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .coordinator import SystemBridgeConfigEntry, SystemBridgeDataUpdateCoordinator
@@ -284,10 +284,10 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
),
SystemBridgeSensorEntityDescription(
key="memory_used_percentage",
translation_key="memory_used_percentage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:memory",
value=lambda data: data.memory.virtual.percent,
),
SystemBridgeSensorEntityDescription(
@@ -380,11 +380,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"filesystem_{partition.mount_point.replace(':', '')}",
name=f"{partition.mount_point} space used",
translation_key="space_used",
translation_placeholders={"partition": partition.mount_point},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:harddisk",
value=(
lambda data, dk=index_device, pk=index_partition: (
partition_usage(data, dk, pk)
@@ -427,10 +427,10 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_resolution_x",
name=f"Display {display.id} resolution x",
translation_key="display_resolution_x",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PIXELS,
icon="mdi:monitor",
value=lambda data, k=index: display_resolution_horizontal(
data, k
),
@@ -441,10 +441,10 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_resolution_y",
name=f"Display {display.id} resolution y",
translation_key="display_resolution_y",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PIXELS,
icon="mdi:monitor",
value=lambda data, k=index: display_resolution_vertical(
data, k
),
@@ -455,12 +455,12 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"display_{display.id}_refresh_rate",
name=f"Display {display.id} refresh rate",
translation_key="display_refresh_rate",
translation_placeholders={"display_id": display.id},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:monitor",
value=lambda data, k=index: display_refresh_rate(data, k),
),
entry.data[CONF_PORT],
@@ -474,13 +474,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_core_clock_speed",
name=f"{gpu.name} clock speed",
translation_key="gpu_core_clock_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:speedometer",
value=lambda data, k=index: gpu_core_clock_speed(data, k),
),
entry.data[CONF_PORT],
@@ -489,13 +489,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_clock_speed",
name=f"{gpu.name} memory clock speed",
translation_key="gpu_memory_clock_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
device_class=SensorDeviceClass.FREQUENCY,
suggested_display_precision=0,
icon="mdi:speedometer",
value=lambda data, k=index: gpu_memory_clock_speed(data, k),
),
entry.data[CONF_PORT],
@@ -504,12 +504,12 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_free",
name=f"{gpu.name} memory free",
translation_key="gpu_memory_free",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
suggested_display_precision=0,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_free(data, k),
),
entry.data[CONF_PORT],
@@ -518,11 +518,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_used_percentage",
name=f"{gpu.name} memory used %",
translation_key="gpu_memory_used_percentage",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_used_percentage(data, k),
),
entry.data[CONF_PORT],
@@ -531,13 +531,13 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_memory_used",
name=f"{gpu.name} memory used",
translation_key="gpu_memory_used",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfInformation.MEGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
suggested_display_precision=0,
icon="mdi:memory",
value=lambda data, k=index: gpu_memory_used(data, k),
),
entry.data[CONF_PORT],
@@ -546,11 +546,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_fan_speed",
name=f"{gpu.name} fan speed",
translation_key="gpu_fan_speed",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
icon="mdi:fan",
value=lambda data, k=index: gpu_fan_speed(data, k),
),
entry.data[CONF_PORT],
@@ -559,7 +559,8 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_power_usage",
name=f"{gpu.name} power usage",
translation_key="gpu_power_usage",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
@@ -571,7 +572,8 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_temperature",
name=f"{gpu.name} temperature",
translation_key="gpu_temperature",
translation_placeholders={"gpu_name": gpu.name},
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@@ -585,11 +587,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"gpu_{gpu.id}_usage_percentage",
name=f"{gpu.name} usage %",
translation_key="gpu_usage_percentage",
translation_placeholders={"gpu_name": gpu.name},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=2,
icon="mdi:percent",
value=lambda data, k=index: gpu_usage_percentage(data, k),
),
entry.data[CONF_PORT],
@@ -605,11 +607,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"processes_load_cpu_{cpu.id}",
name=f"Load CPU {cpu.id}",
translation_key="processes_load_cpu",
translation_placeholders={"cpu_id": str(cpu.id)},
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
suggested_display_precision=2,
value=lambda data, k=cpu.id: cpu_usage_per_cpu(data, k),
),
@@ -619,11 +621,11 @@ async def async_setup_entry(
coordinator,
SystemBridgeSensorEntityDescription(
key=f"cpu_power_core_{cpu.id}",
name=f"CPU Core {cpu.id} Power",
translation_key="cpu_power_core",
translation_placeholders={"cpu_id": str(cpu.id)},
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:chip",
suggested_display_precision=2,
value=lambda data, k=cpu.id: cpu_power_per_cpu(data, k),
),
@@ -653,8 +655,6 @@ class SystemBridgeSensor(SystemBridgeEntity, SensorEntity):
description.key,
)
self.entity_description = description
if description.name is not UNDEFINED:
self._attr_has_entity_name = False
@property
def native_value(self) -> StateType:
@@ -89,3 +89,4 @@ power_command:
- "restart"
- "shutdown"
- "sleep"
translation_key: "power_command"
@@ -54,6 +54,9 @@
"boot_time": {
"name": "Boot time"
},
"cpu_power_core": {
"name": "CPU core {cpu_id} power"
},
"cpu_power_package": {
"name": "CPU package power"
},
@@ -66,9 +69,45 @@
"cpu_voltage": {
"name": "CPU voltage"
},
"display_refresh_rate": {
"name": "Display {display_id} refresh rate"
},
"display_resolution_x": {
"name": "Display {display_id} resolution x"
},
"display_resolution_y": {
"name": "Display {display_id} resolution y"
},
"displays_connected": {
"name": "Displays connected"
},
"gpu_core_clock_speed": {
"name": "{gpu_name} clock speed"
},
"gpu_fan_speed": {
"name": "{gpu_name} fan speed"
},
"gpu_memory_clock_speed": {
"name": "{gpu_name} memory clock speed"
},
"gpu_memory_free": {
"name": "{gpu_name} memory free"
},
"gpu_memory_used": {
"name": "{gpu_name} memory used"
},
"gpu_memory_used_percentage": {
"name": "{gpu_name} memory used %"
},
"gpu_power_usage": {
"name": "{gpu_name} power usage"
},
"gpu_temperature": {
"name": "{gpu_name} temperature"
},
"gpu_usage_percentage": {
"name": "{gpu_name} usage %"
},
"kernel": {
"name": "Kernel"
},
@@ -81,6 +120,9 @@
"memory_used": {
"name": "Memory used"
},
"memory_used_percentage": {
"name": "Memory used %"
},
"os": {
"name": "Operating system"
},
@@ -90,6 +132,12 @@
"processes": {
"name": "Processes"
},
"processes_load_cpu": {
"name": "Load CPU {cpu_id}"
},
"space_used": {
"name": "{partition} space used"
},
"version": {
"name": "Version"
},
@@ -130,6 +178,18 @@
"title": "System Bridge upgrade required"
}
},
"selector": {
"power_command": {
"options": {
"hibernate": "Hibernate",
"lock": "Lock",
"logout": "Logout",
"restart": "[%key:common::action::restart%]",
"shutdown": "Shutdown",
"sleep": "Sleep"
}
}
},
"services": {
"get_process_by_id": {
"description": "Gets a process by the ID.",
@@ -32,6 +32,7 @@ class SystemBridgeUpdateEntity(SystemBridgeEntity, UpdateEntity):
_attr_has_entity_name = True
_attr_title = "System Bridge"
_attr_name = None
def __init__(
self,
@@ -44,7 +45,6 @@ class SystemBridgeUpdateEntity(SystemBridgeEntity, UpdateEntity):
api_port,
"update",
)
self._attr_name = coordinator.data.system.hostname
@property
def installed_version(self) -> str | None:
@@ -58,7 +58,7 @@ send_message:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -101,7 +101,7 @@ send_chat_action:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -195,7 +195,7 @@ send_photo:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -287,7 +287,7 @@ send_media_group:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -372,7 +372,7 @@ send_sticker:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -466,7 +466,7 @@ send_animation:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -560,7 +560,7 @@ send_video:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -645,7 +645,7 @@ send_voice:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -739,7 +739,7 @@ send_document:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -804,7 +804,7 @@ send_location:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -861,7 +861,7 @@ send_poll:
selector:
number:
mode: box
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -913,7 +913,7 @@ edit_message:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -991,7 +991,7 @@ edit_message_media:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1028,7 +1028,7 @@ edit_caption:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1061,7 +1061,7 @@ edit_replymarkup:
["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
selector:
object:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1108,7 +1108,7 @@ delete_message:
example: "{{ trigger.event.data.message.message_id }}"
selector:
text:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1129,7 +1129,7 @@ leave_chat:
filter:
domain: notify
integration: telegram_bot
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1164,7 +1164,7 @@ set_message_reaction:
required: false
selector:
boolean:
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -1233,7 +1233,7 @@ send_message_draft:
- "markdownv2"
- "plain_text"
translation_key: "parse_mode"
advanced:
additional_fields:
collapsed: true
fields:
config_entry_id:
@@ -367,8 +367,8 @@
},
"name": "Delete message",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -425,8 +425,8 @@
},
"name": "Edit caption",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -472,8 +472,8 @@
},
"name": "Edit message",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -535,8 +535,8 @@
},
"name": "Edit message media",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -569,8 +569,8 @@
},
"name": "Edit reply markup",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -592,8 +592,8 @@
},
"name": "Leave chat",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -671,8 +671,8 @@
},
"name": "Send animation",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -705,8 +705,8 @@
},
"name": "Send chat action",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -784,8 +784,8 @@
},
"name": "Send document",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -842,8 +842,8 @@
},
"name": "Send location",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -889,8 +889,8 @@
},
"name": "Send media group",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -952,8 +952,8 @@
},
"name": "Send message",
"sections": {
"advanced": {
"name": "Advanced"
"additional_fields": {
"name": "Additional options"
}
}
},
@@ -991,8 +991,8 @@
},
"name": "Send message draft",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -1070,8 +1070,8 @@
},
"name": "Send photo",
"sections": {
"advanced": {
"name": "Advanced"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "URL options"
@@ -1128,8 +1128,8 @@
},
"name": "Send poll",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
},
@@ -1203,8 +1203,8 @@
},
"name": "Send sticker",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1285,8 +1285,8 @@
},
"name": "Send video",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1363,8 +1363,8 @@
},
"name": "Send voice",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
},
"url_options": {
"name": "[%key:component::telegram_bot::services::send_photo::sections::url_options::name%]"
@@ -1401,8 +1401,8 @@
},
"name": "Set message reaction",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
"additional_fields": {
"name": "[%key:component::telegram_bot::services::send_message::sections::additional_fields::name%]"
}
}
}
@@ -497,7 +497,7 @@
"default": "mdi:battery-clock"
},
"forward_collision_warning": {
"default": "mdi:car-crash",
"default": "mdi:car-emergency",
"state": {
"average": "mdi:alert-circle",
"early": "mdi:alert-octagon",
@@ -634,7 +634,7 @@
"default": "mdi:key"
},
"pedal_position": {
"default": "mdi:pedestal"
"default": "mdi:gauge"
},
"powershare_hours_left": {
"default": "mdi:clock-time-eight-outline"
@@ -794,7 +794,7 @@
"service": "mdi:calendar-plus"
},
"add_precondition_schedule": {
"service": "mdi:hvac-outline"
"service": "mdi:hvac"
},
"navigation_gps_request": {
"service": "mdi:crosshairs-gps"
@@ -803,7 +803,7 @@
"service": "mdi:calendar-minus"
},
"remove_precondition_schedule": {
"service": "mdi:hvac-off-outline"
"service": "mdi:hvac-off"
},
"set_scheduled_charging": {
"service": "mdi:timeline-clock-outline"
@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import PLATFORMS
from .const import DOMAIN, PLATFORMS
from .coordinator import UptimeRobotConfigEntry, UptimeRobotDataUpdateCoordinator
@@ -15,9 +15,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: UptimeRobotConfigEntry)
"""Set up UptimeRobot from a config entry."""
key: str = entry.data[CONF_API_KEY]
if key.startswith(("ur", "m")):
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(
"Wrong API key type detected, use the 'main' API key"
translation_domain=DOMAIN,
translation_key="api_key_wrong_type",
)
uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass))
@@ -48,11 +48,16 @@ class UptimeRobotDataUpdateCoordinator(
try:
response = await self.api.async_get_monitors()
except UptimeRobotAuthenticationException as exception:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(exception) from exception
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="api_authentication_exception",
) from exception
except UptimeRobotException as exception:
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed(exception) from exception
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="api_generic_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
if TYPE_CHECKING:
assert isinstance(response.data, list)
@@ -57,7 +57,16 @@
}
},
"exceptions": {
"api_exception": {
"api_authentication_exception": {
"message": "API authentication failed, please check your API key"
},
"api_generic_exception": {
"message": "API error: {error}"
},
"api_key_wrong_type": {
"message": "Wrong API key type detected, use the 'main' API key"
},
"api_switch_exception": {
"message": "Could not turn on/off monitoring: {error}"
}
}
@@ -33,7 +33,7 @@ def uptimerobot_api_call[_T: UptimeRobotEntity, **_P](
except UptimeRobotException as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_exception",
translation_key="api_switch_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
@@ -68,7 +68,7 @@
"state": {
"lightning": "mdi:weather-lightning-rainy",
"rain": "mdi:weather-rainy",
"rain_snow": "mdi:weather-snoy-rainy",
"rain_snow": "mdi:weather-snowy-rainy",
"snow": "mdi:weather-snowy"
}
},
+4 -2
View File
@@ -46,8 +46,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebOsTvConfigEntry) -> b
try:
await client.connect()
except WebOsTvPairError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_failed",
) from err
# If pairing request accepted there will be no error
# Update the stored key without triggering reauth
@@ -6,6 +6,7 @@ from homeassistant.components.device_automation import (
DEVICE_TRIGGER_BASE_SCHEMA,
InvalidDeviceAutomationConfig,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_DEVICE_ID, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -13,10 +14,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import DOMAIN, trigger
from .helpers import (
async_get_client_by_device_entry,
async_get_device_entry_by_device_id,
)
from .helpers import async_get_device_entry_by_device_id
from .triggers.turn_on import (
PLATFORM_TYPE as TURN_ON_PLATFORM_TYPE,
async_get_turn_on_trigger,
@@ -40,10 +38,31 @@ async def async_validate_trigger_config(
device_id = config[CONF_DEVICE_ID]
try:
device = async_get_device_entry_by_device_id(hass, device_id)
async_get_client_by_device_entry(hass, device)
except ValueError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise InvalidDeviceAutomationConfig(err) from err
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_not_valid",
translation_placeholders={"device_id": device_id},
) from err
for config_entry_id in device.config_entries:
if (
entry := hass.config_entries.async_get_entry(config_entry_id)
) and entry.domain == DOMAIN:
if entry.state is ConfigEntryState.LOADED:
break
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_config_entry_not_loaded",
translation_placeholders={"device_id": device.id},
)
else:
raise InvalidDeviceAutomationConfig(
translation_domain=DOMAIN,
translation_key="device_not_valid",
translation_placeholders={"device_id": device.id},
)
return config
+1 -26
View File
@@ -4,7 +4,7 @@ import logging
from aiowebostv import WebOsClient, WebOsTvState
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
@@ -56,31 +56,6 @@ def async_get_device_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> s
return entity_entry.device_id
@callback
def async_get_client_by_device_entry(
hass: HomeAssistant, device: DeviceEntry
) -> WebOsClient:
"""Get WebOsClient from Device Registry by device entry.
Raises ValueError if client is not found.
"""
for config_entry_id in device.config_entries:
entry: WebOsTvConfigEntry | None = hass.config_entries.async_get_entry(
config_entry_id
)
if entry and entry.domain == DOMAIN:
if entry.state is ConfigEntryState.LOADED:
return entry.runtime_data
raise ValueError(
f"Device {device.id} is not from a loaded {DOMAIN} config entry"
)
raise ValueError(
f"Device {device.id} is not from an existing {DOMAIN} config entry"
)
def get_sources(tv_state: WebOsTvState) -> list[str]:
"""Construct sources list."""
sources = []
@@ -46,9 +46,18 @@
}
},
"exceptions": {
"auth_failed": {
"message": "Pairing failed, make sure to accept the pairing request on your TV."
},
"communication_error": {
"message": "Communication error while calling {func} for device {name}: {error}"
},
"device_config_entry_not_loaded": {
"message": "The LG webOS TV integration for device {device_id} is not loaded."
},
"device_not_valid": {
"message": "Device {device_id} is not a valid LG webOS TV device."
},
"device_off": {
"message": "Error calling {func} for device {name}: Device is off and cannot be controlled."
},
@@ -28,7 +28,7 @@ from miio.integrations.humidifier.zhimi.airhumidifier_miot import (
)
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory
from homeassistant.const import ATTR_MODE, CONF_DEVICE, CONF_MODEL, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@@ -65,8 +65,6 @@ from .typing import XiaomiMiioConfigEntry
ATTR_DISPLAY_ORIENTATION = "display_orientation"
ATTR_LED_BRIGHTNESS = "led_brightness"
ATTR_PTC_LEVEL = "ptc_level"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
_LOGGER = logging.getLogger(__name__)
@@ -25,6 +25,7 @@ from homeassistant.components.switch import (
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
ATTR_MODEL,
ATTR_TEMPERATURE,
CONF_DEVICE,
CONF_HOST,
@@ -149,8 +150,6 @@ ATTR_LED = "led"
ATTR_IONIZER = "ionizer"
ATTR_ANION = "anion"
ATTR_LOAD_POWER = "load_power"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL = "model"
ATTR_POWER = "power"
ATTR_POWER_MODE = "power_mode"
ATTR_POWER_PRICE = "power_price"
+1 -1
View File
@@ -35,7 +35,7 @@ file-read-backwards==2.0.0
fnv-hash-fast==2.0.2
go2rtc-client==0.4.0
ha-ffmpeg==3.2.2
habluetooth==6.3.0
habluetooth==6.2.0
hass-nabucasa==2.2.0
hassil==3.5.0
home-assistant-bluetooth==2.0.0
@@ -0,0 +1,113 @@
"""Checker for invalid MDI icon references.
Validates that ``mdi:`` icon references in integration code and
``icons.json`` files refer to icons that actually exist in the
Material Design Icons set.
- ``E7409``: MDI icon reference not found in Python code
- ``E7410``: MDI icon reference not found in icons.json
"""
import re
from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter
from pylint_home_assistant.generated.mdi_icons import MDI_ICONS
from pylint_home_assistant.helpers.icons import collect_mdi_icons, load_icons
from pylint_home_assistant.helpers.module_info import parse_module
# Matches strings that look like intentional icon name attempts
# (letters, digits, hyphens, underscores). Rejects format templates
# (%s, {}, {name}), empty names, and other dynamic patterns.
_LOOKS_LIKE_ICON_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*[a-zA-Z0-9]$")
class MdiIconsChecker(BaseChecker):
"""Checker for invalid MDI icon references."""
name = "home_assistant_mdi_icons"
priority = -1
msgs = {
"E7409": (
"MDI icon '%s' does not exist in the Material Design Icons set",
"home-assistant-mdi-icon-not-found",
"Used when an integration references an MDI icon in Python "
"code that does not exist. Check the icon name at "
"https://pictogrammers.com/library/mdi/",
),
"E7410": (
"MDI icon '%s' in icons.json does not exist in the "
"Material Design Icons set",
"home-assistant-mdi-icon-json-not-found",
"Used when an integration's icons.json references an MDI "
"icon that does not exist. Check the icon name at "
"https://pictogrammers.com/library/mdi/",
),
}
options = ()
_in_integration: bool
_checked_icons_json: set[str]
def open(self) -> None:
"""Initialize per-run state."""
self._checked_icons_json = set()
def visit_module(self, node: nodes.Module) -> None:
"""Check icons.json and track integration context."""
parsed = parse_module(node.name)
self._in_integration = parsed is not None
if parsed is None:
return
# Only check icons.json once per integration
if parsed.domain in self._checked_icons_json:
return
self._checked_icons_json.add(parsed.domain)
icons_data = load_icons(node)
if icons_data is None:
return
mdi_refs = collect_mdi_icons(icons_data)
for icon_ref in sorted(mdi_refs):
icon_name = icon_ref[4:] # Strip "mdi:" prefix
if icon_name not in MDI_ICONS:
self.add_message(
"home-assistant-mdi-icon-json-not-found",
node=node,
args=(icon_ref,),
)
def visit_const(self, node: nodes.Const) -> None:
"""Check string constants for invalid MDI icon references."""
if not self._in_integration:
return
if not isinstance(node.value, str):
return
if not node.value.startswith("mdi:"):
return
icon_name = node.value[4:] # Strip "mdi:" prefix
# Only check names that look like intentional icon name attempts.
# This skips f-string fragments, format templates (%s, {}),
# partial names, and other dynamic patterns.
if not _LOOKS_LIKE_ICON_NAME.match(icon_name):
return
if icon_name not in MDI_ICONS:
self.add_message(
"home-assistant-mdi-icon-not-found",
node=node,
args=(node.value,),
)
def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(MdiIconsChecker(linter))
@@ -0,0 +1 @@
"""Generated files for the pylint Home Assistant plugin."""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
"""Helpers for reading integration icon files."""
import contextlib
from astroid import nodes
import orjson
from .integration import get_integration_dir
_icons_cache: dict[str, dict | None] = {}
def clear_icons_cache() -> None:
"""Clear the icons cache (used by tests)."""
_icons_cache.clear()
def load_icons(module: nodes.Module) -> dict | None:
"""Load and cache the icons.json for the current integration.
Returns the parsed JSON as a dict, or ``None`` if not found.
"""
integration_dir = get_integration_dir(module)
if integration_dir is None:
return None
cache_key = str(integration_dir)
if cache_key in _icons_cache:
return _icons_cache[cache_key]
icons_path = integration_dir / "icons.json"
result: dict | None = None
if icons_path.exists():
with contextlib.suppress(orjson.JSONDecodeError, OSError):
parsed = orjson.loads(icons_path.read_bytes())
if isinstance(parsed, dict):
result = parsed
_icons_cache[cache_key] = result
return result
def collect_mdi_icons(
data: dict | list | str, icons: set[str] | None = None
) -> set[str]:
"""Recursively collect all mdi: icon references from a data structure."""
if icons is None:
icons = set()
if isinstance(data, str):
if data.startswith("mdi:"):
icons.add(data)
elif isinstance(data, dict):
for value in data.values():
collect_mdi_icons(value, icons)
elif isinstance(data, list):
for item in data:
collect_mdi_icons(item, icons)
return icons
+3 -3
View File
@@ -1210,7 +1210,7 @@ ha-xthings-cloud==1.0.5
habiticalib==0.4.7
# homeassistant.components.bluetooth
habluetooth==6.3.0
habluetooth==6.2.0
# homeassistant.components.hanna
hanna-cloud==0.0.7
@@ -2641,7 +2641,7 @@ python-google-weather-api==0.0.6
python-homeassistant-analytics==0.9.0
# homeassistant.components.homewizard
python-homewizard-energy==10.0.1
python-homewizard-energy==10.1.0
# homeassistant.components.hp_ilo
python-hpilo==4.4.3
@@ -2871,7 +2871,7 @@ renault-api==0.5.10
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.19.1
reolink-aio==0.20.0
# homeassistant.components.radio_frequency
rf-protocols==3.2.0
View File
+2
View File
@@ -23,6 +23,7 @@ from . import (
json,
labs,
manifest,
mdi_icons,
metadata,
mqtt,
mypy_config,
@@ -65,6 +66,7 @@ INTEGRATION_PLUGINS = [
HASS_PLUGINS = [
core_files,
docker,
mdi_icons,
mypy_config,
metadata,
]
+76
View File
@@ -0,0 +1,76 @@
"""Generate MDI icons file for the pylint plugin."""
from importlib.metadata import PackageNotFoundError, version
from importlib.resources import files
import json
from .model import Config, Integration
from .serializer import format_python_namespace
_TARGET = "pylint/plugins/pylint_home_assistant/generated/mdi_icons.py"
def _get_frontend_version() -> str | None:
"""Get the installed home-assistant-frontend version."""
try:
return version("home-assistant-frontend")
except PackageNotFoundError:
return None
def _load_mdi_icons() -> set[str]:
"""Load the MDI icon names from the frontend package."""
try:
mdi_dir = files("hass_frontend") / "static" / "mdi"
icon_list_path = mdi_dir / "iconList.json"
data = json.loads(icon_list_path.read_text(encoding="utf-8"))
return {icon["name"] for icon in data}
except ImportError, FileNotFoundError, json.JSONDecodeError, KeyError:
return set()
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Validate the generated MDI icons file is up to date."""
frontend_version = _get_frontend_version()
if frontend_version is None:
return
icons = _load_mdi_icons()
if not icons:
config.add_error(
"mdi_icons",
"Could not load MDI icons from home-assistant-frontend",
)
return
content = format_python_namespace(
{
"FRONTEND_VERSION": frontend_version,
"MDI_ICONS": icons,
},
annotations={
"FRONTEND_VERSION": "Final[str]",
"MDI_ICONS": "Final[set[str]]",
},
)
config.cache["mdi_icons_content"] = content
if config.specific_integrations:
return
target_path = config.root / _TARGET
if not target_path.exists() or target_path.read_text() != content:
config.add_error(
"mdi_icons",
f"File {_TARGET} is not up to date. Run python3 -m script.hassfest",
fixable=True,
)
def generate(integrations: dict[str, Integration], config: Config) -> None:
"""Generate MDI icons file."""
if "mdi_icons_content" not in config.cache:
return
target_path = config.root / _TARGET
target_path.write_text(config.cache["mdi_icons_content"])
+316 -35
View File
@@ -4,6 +4,8 @@
import argparse
from concurrent.futures import ProcessPoolExecutor
from dataclasses import dataclass, field
import hashlib
import json
from math import ceil
import os
from pathlib import Path
@@ -15,13 +17,15 @@ from typing import Final
# place to subdivide to keep each pytest invocation roughly equal in size.
_FAN_OUT_DIRS: Final = frozenset({"components"})
# Cache file format version; bump on any incompatible schema change so old
# caches are ignored rather than misread.
_CACHE_VERSION: Final = 2
class Bucket:
"""Class to hold bucket."""
def __init__(
self,
):
def __init__(self) -> None:
"""Initialize bucket."""
self.total_tests = 0
self._paths: list[str] = []
@@ -81,9 +85,9 @@ class BucketHolder:
if not test_folder.added_to_bucket:
raise ValueError("Not all tests are added to a bucket")
def create_ouput_file(self) -> None:
def create_output_file(self) -> None:
"""Create output file."""
with Path("pytest_buckets.txt").open("w") as file:
with Path("pytest_buckets.txt").open("w", encoding="utf-8") as file:
for idx, bucket in enumerate(self._buckets):
print(f"Bucket {idx + 1} has {bucket.total_tests} tests")
file.write(bucket.get_paths_line())
@@ -184,9 +188,10 @@ def _collect_batch(paths: list[Path]) -> tuple[str, str, int]:
def _iter_eligible_children(path: Path) -> list[Path]:
"""Return immediate children of ``path`` that pytest should collect.
Filters out hidden/dunder entries, non-``test_*.py`` files (so helper
modules like ``conftest.py`` and ``common.py`` are not passed as
explicit collection targets), and pycache-style directories.
Skips entries whose name starts with ``.`` or ``_`` (hidden dirs,
``__pycache__``, private helpers), and non-``test_*.py`` files (so
helper modules like ``conftest.py`` and ``common.py`` are not passed
as explicit collection targets).
"""
children: list[Path] = []
for entry in sorted(path.iterdir()):
@@ -216,44 +221,314 @@ def _enumerate_batch_paths(path: Path) -> list[Path]:
return paths
def collect_tests(path: Path) -> TestFolder:
"""Collect all tests."""
batch_paths = _enumerate_batch_paths(path)
if not batch_paths:
print(f"No eligible test paths found under {path}")
sys.exit(1)
workers = min(len(batch_paths), os.cpu_count() or 1) or 1
# Round-robin chunking keeps batches roughly balanced when path
# ordering correlates with test size.
batches = [batch_paths[i::workers] for i in range(workers)]
def _hash_file(path: Path) -> str:
"""Return a short content hash for ``path``."""
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
def _walk_test_tree(root: Path) -> tuple[list[Path], list[Path]]:
"""Walk ``root`` once and return (test files, fixture files).
Test files are the ``test_*.py`` modules that pytest will collect.
Fixture files are every other ``.py`` under ``root`` — ``conftest.py``
plus helper modules like ``common.py``. Helpers go into the
invalidation hash because they often hold the ``VALUES`` lists that
test files import for ``@pytest.mark.parametrize``: editing one
changes a test's collected count even though the test file itself is
untouched.
Uses ``os.walk`` rather than ``Path.rglob`` because it's ~2x faster on
a 5000-file tree, and subdirectories whose names start with ``.`` or
``_`` are pruned instead of visited (hidden dirs, ``__pycache__``,
private helpers). Doing both walks in one pass keeps total tree I/O
down.
"""
test_files: list[Path] = []
fixtures: list[Path] = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if not d.startswith((".", "_"))]
base = Path(dirpath)
for name in filenames:
if not name.endswith(".py"):
continue
if name.startswith("test_"):
test_files.append(base / name)
else:
fixtures.append(base / name)
test_files.sort()
fixtures.sort()
return test_files, fixtures
def _find_ancestor_conftests(root: Path) -> list[Path]:
"""Return ancestor ``conftest.py`` files that pytest would still apply.
Pytest walks up from each test file looking for conftests; when
``root`` is a subtree (eg ``tests/components``) the conftests above
it (eg ``tests/conftest.py``) still affect parametrization, so they
must contribute to the invalidation hash too. Stops at the first
ancestor without a ``conftest.py``.
"""
ancestors: list[Path] = []
current = root.resolve().parent
while True:
conftest = current / "conftest.py"
if not conftest.is_file():
break
ancestors.append(conftest)
if current == current.parent:
break
current = current.parent
return ancestors
def _compute_invalidation_hash(root: Path, fixtures: list[Path]) -> str:
"""Return a hash that changes whenever any file in ``fixtures`` changes.
Any change to a fixture file (conftests, helper modules like
``common.py``, ancestor conftests) invalidates the entire test-count
cache. This is coarse but safe: any of these can shift fixture
parametrization in ways the cache cannot otherwise detect, so we
just re-collect everything.
Paths are encoded with ``os.path.relpath`` so the hash stays stable
across machines and also covers ancestor conftests above ``root``
(whose ``relative_to(root)`` would fail).
"""
digest = hashlib.sha256()
for fixture in fixtures:
digest.update(os.path.relpath(fixture, root).encode())
digest.update(b"\0")
digest.update(fixture.read_bytes())
digest.update(b"\0")
return digest.hexdigest()
@dataclass
class _CacheEntry:
"""Cached test count for a single file."""
hash: str
count: int
@dataclass
class _Cache:
"""Mapping of test file path → cached entry, plus invalidation key."""
invalidation_hash: str
entries: dict[str, _CacheEntry]
@classmethod
def empty(cls, invalidation_hash: str = "") -> _Cache:
"""Return a new empty cache."""
return cls(invalidation_hash=invalidation_hash, entries={})
@classmethod
def load(cls, path: Path, current_invalidation_hash: str) -> _Cache:
"""Load cache from ``path`` and invalidate it on schema/fixture drift.
Any failure (missing file, bad JSON, version drift, fixture drift)
returns an empty cache so the script just falls back to a full
collection. This is the self-healing path.
"""
try:
raw = json.loads(path.read_bytes())
except OSError, ValueError:
return cls.empty(current_invalidation_hash)
if not isinstance(raw, dict) or raw.get("version") != _CACHE_VERSION:
return cls.empty(current_invalidation_hash)
if raw.get("invalidation_hash") != current_invalidation_hash:
return cls.empty(current_invalidation_hash)
files = raw.get("files")
if not isinstance(files, dict):
return cls.empty(current_invalidation_hash)
entries: dict[str, _CacheEntry] = {}
for key, value in files.items():
if (
not isinstance(value, dict)
or not isinstance(value.get("hash"), str)
or not isinstance(value.get("count"), int)
):
# Skip malformed entries instead of discarding the whole cache.
continue
entries[key] = _CacheEntry(hash=value["hash"], count=value["count"])
return cls(invalidation_hash=current_invalidation_hash, entries=entries)
def save(self, path: Path) -> None:
"""Write the cache to ``path``."""
path.write_text(
json.dumps(
{
"version": _CACHE_VERSION,
"invalidation_hash": self.invalidation_hash,
"files": {
key: {"hash": entry.hash, "count": entry.count}
for key, entry in sorted(self.entries.items())
},
},
indent=2,
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)
def _resolve_from_cache(
test_files: list[Path],
cache: _Cache,
root: Path,
) -> tuple[dict[Path, _CacheEntry], dict[Path, str]]:
"""Split ``test_files`` into ``(cached_entries, miss_hashes)``.
A file is served from cache when its content hash matches what we
previously stored; otherwise it is queued for re-collection. Each
file is hashed exactly once: hits carry the stored hash forward,
misses carry the just-computed hash so the rebuild step doesn't
re-read the same bytes a second time.
"""
hits: dict[Path, _CacheEntry] = {}
miss_hashes: dict[Path, str] = {}
for file in test_files:
file_hash = _hash_file(file)
entry = cache.entries.get(str(file.relative_to(root)))
if entry is not None and entry.hash == file_hash:
hits[file] = entry
else:
miss_hashes[file] = file_hash
return hits, miss_hashes
def _run_collect_batches(paths: list[Path]) -> list[tuple[str, str, int]]:
"""Run pytest --collect-only across ``paths`` using a process pool."""
workers = min(len(paths), os.cpu_count() or 1) or 1
batches = [paths[i::workers] for i in range(workers)]
if workers == 1:
results = [_collect_batch(batches[0])]
else:
with ProcessPoolExecutor(max_workers=workers) as executor:
results = list(executor.map(_collect_batch, batches))
return [_collect_batch(batches[0])]
with ProcessPoolExecutor(max_workers=workers) as executor:
return list(executor.map(_collect_batch, batches))
folder = TestFolder(path)
for stdout, stderr, returncode in results:
def _parse_collect_output(stdout: str) -> dict[Path, int]:
"""Parse ``pytest --collect-only -qq`` output into ``{path: count}``."""
counts: dict[Path, int] = {}
for line in stdout.splitlines():
if not line.strip():
continue
file_path, _, total_tests = line.partition(": ")
if not file_path or not total_tests:
raise ValueError(f"Unexpected line: {line}")
counts[Path(file_path)] = int(total_tests)
return counts
def _run_pytest_collect(paths: list[Path]) -> dict[Path, int]:
"""Run pytest --collect-only across ``paths`` and parse the output."""
counts: dict[Path, int] = {}
for stdout, stderr, returncode in _run_collect_batches(paths):
if returncode != 0:
print("Failed to collect tests:")
print(stderr)
print(stdout)
sys.exit(1)
for line in stdout.splitlines():
if not line.strip():
continue
file_path, _, total_tests = line.partition(": ")
if not file_path or not total_tests:
print(f"Unexpected line: {line}")
sys.exit(1)
try:
counts.update(_parse_collect_output(stdout))
except ValueError as err:
print(err)
sys.exit(1)
return counts
file = TestFile(int(total_tests), Path(file_path))
folder.add_test_file(file)
def _build_folder(root: Path, counts: dict[Path, int]) -> TestFolder:
"""Build a ``TestFolder`` from a flat ``{path: count}`` mapping.
Files reported with zero tests are skipped so they don't enter
bucketing (helper modules named ``test_*.py`` with no test functions
look like test files to the walker but pytest returns nothing for
them).
"""
folder = TestFolder(root)
for file_path, count in counts.items():
if count:
folder.add_test_file(TestFile(count, file_path))
return folder
def _exit_if_empty(paths: list[Path], root: Path) -> None:
"""Exit with a clear message when no eligible test paths were found."""
if not paths:
print(f"No eligible test paths found under {root}")
sys.exit(1)
def _collect_tests_uncached(path: Path) -> TestFolder:
"""Collect tests by handing pytest the top-level directories.
Skips the tree walk and per-file hashing; used when no cache file is
requested so the script behaves like the pre-cache implementation.
"""
batch_paths = _enumerate_batch_paths(path)
_exit_if_empty(batch_paths, path)
return _build_folder(path, _run_pytest_collect(batch_paths))
def _collect_tests_cached(path: Path, cache_path: Path) -> TestFolder:
"""Collect tests using an on-disk cache for incremental updates."""
all_test_files, fixtures = _walk_test_tree(path)
_exit_if_empty(all_test_files, path)
# Include ancestor conftests so a subtree run (eg tests/components)
# still invalidates when tests/conftest.py changes.
all_fixtures = _find_ancestor_conftests(path) + fixtures
invalidation_hash = _compute_invalidation_hash(path, all_fixtures)
cache = _Cache.load(cache_path, invalidation_hash)
hits, miss_hashes = _resolve_from_cache(all_test_files, cache, path)
print(
f"Cache: {len(hits)} hits / {len(miss_hashes)} misses"
f" / {len(all_test_files)} total"
)
new_counts: dict[Path, int] = {}
if miss_hashes:
# On a full cold-cache run, hand pytest the top-level directories
# instead of 5000+ individual file paths: pytest walks dirs much
# faster than it resolves each file argument. Once any cache hits
# exist, use file-level collection so we only re-collect the diff.
collect_paths = _enumerate_batch_paths(path) if not hits else list(miss_hashes)
new_counts = _run_pytest_collect(collect_paths)
# Walk the full set of test files once and decide each file's entry:
# hits keep their stored entry (and verified hash), misses build a
# fresh entry from the resolve-time hash plus the freshly collected
# count. Files in misses that pytest returned no count for are
# stored as 0 so they stop re-collecting on the next run.
entries: dict[str, _CacheEntry] = {}
counts: dict[Path, int] = {}
for file in all_test_files:
if (entry := hits.get(file)) is None:
entry = _CacheEntry(hash=miss_hashes[file], count=new_counts.get(file, 0))
entries[str(file.relative_to(path))] = entry
counts[file] = entry.count
_Cache(invalidation_hash=invalidation_hash, entries=entries).save(cache_path)
return _build_folder(path, counts)
def collect_tests(path: Path, cache_path: Path | None = None) -> TestFolder:
"""Collect all tests, using an on-disk cache when ``cache_path`` is set."""
if cache_path is None:
return _collect_tests_uncached(path)
if path.is_file():
# The cache keys on conftest_hash, but a single file root has no
# ancestor conftests to walk and the hash would always be empty,
# which would let stale counts survive conftest edits. Skip the
# cache for the file-root case rather than silently mis-caching.
print(f"--cache ignored: {path} is a single file")
return _collect_tests_uncached(path)
return _collect_tests_cached(path, cache_path)
def main() -> None:
"""Execute script."""
parser = argparse.ArgumentParser(description="Split tests into n buckets.")
@@ -276,11 +551,17 @@ def main() -> None:
help="Path to the test files to split into buckets",
type=Path,
)
parser.add_argument(
"--cache",
help="Path to a JSON file used to cache per-file test counts",
type=Path,
default=None,
)
arguments = parser.parse_args()
print("Collecting tests...")
tests = collect_tests(arguments.path)
tests = collect_tests(arguments.path, arguments.cache)
tests_per_bucket = ceil(tests.total_tests / arguments.bucket_count)
bucket_holder = BucketHolder(tests_per_bucket, arguments.bucket_count)
@@ -290,7 +571,7 @@ def main() -> None:
print(f"Total tests: {tests.total_tests}")
print(f"Estimated tests per bucket: {tests_per_bucket}")
bucket_holder.create_ouput_file()
bucket_holder.create_output_file()
if __name__ == "__main__":
@@ -102,37 +102,37 @@ async def test_login(hass: HomeAssistant) -> None:
provider = hass.auth.auth_providers[0]
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "mfa"
assert result["data_schema"].schema.get("pin") is str
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"pin": "invalid-code"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_code"
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"pin": "123456"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"].id == "mock-id"
@@ -149,9 +149,9 @@ async def test_setup_flow(hass: HomeAssistant) -> None:
flow = await auth_module.async_setup_flow("new-user")
result = await flow.async_step_init()
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await flow.async_step_init({"pin": "abcdefg"})
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert auth_module._data[1]["user_id"] == "new-user"
assert auth_module._data[1]["pin"] == "abcdefg"
+15 -15
View File
@@ -137,25 +137,25 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
provider = hass.auth.auth_providers[0]
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "mfa"
assert result["data_schema"].schema.get("code") is str
@@ -173,7 +173,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"code": "invalid-code"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "mfa"
assert result["errors"]["base"] == "invalid_code"
@@ -191,7 +191,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"code": "invalid-code"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "mfa"
assert result["errors"]["base"] == "invalid_code"
@@ -199,7 +199,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"code": "invalid-code"}
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["type"] is data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "too_many_retry"
# wait service call finished
@@ -207,13 +207,13 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
# restart login
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "mfa"
assert result["data_schema"].schema.get("code") is str
@@ -231,7 +231,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"code": MOCK_CODE}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"].id == "mock-id"
@@ -246,7 +246,7 @@ async def test_setup_user_notify_service(hass: HomeAssistant) -> None:
flow = await notify_auth_module.async_setup_flow("test-user")
step = await flow.async_step_init()
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
assert step["step_id"] == "init"
schema = step["data_schema"]
schema({"notify_service": "test2"})
@@ -277,7 +277,7 @@ async def test_setup_user_notify_service(hass: HomeAssistant) -> None:
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
step = await flow.async_step_init({"notify_service": "test1"})
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
assert step["step_id"] == "setup"
# wait service call finished
@@ -357,7 +357,7 @@ async def test_setup_user_no_notify_service(hass: HomeAssistant) -> None:
flow = await notify_auth_module.async_setup_flow("test-user")
step = await flow.async_step_init()
assert step["type"] == data_entry_flow.FlowResultType.ABORT
assert step["type"] is data_entry_flow.FlowResultType.ABORT
assert step["reason"] == "no_available_service"
@@ -394,13 +394,13 @@ async def test_not_raise_exception_when_service_not_exist(hass: HomeAssistant) -
provider = hass.auth.auth_providers[0]
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
with patch("pyotp.HOTP.at", return_value=MOCK_CODE):
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["type"] is data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "unknown_error"
# wait service call finished
+6 -6
View File
@@ -95,24 +95,24 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
provider = hass.auth.auth_providers[0]
result = await hass.auth.login_flow.async_init((provider.type, provider.id))
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "incorrect-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "incorrect-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "mfa"
assert result["data_schema"].schema.get("code") is str
@@ -120,7 +120,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"code": "invalid-code"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "mfa"
assert result["errors"]["base"] == "invalid_code"
@@ -128,7 +128,7 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None:
result = await hass.auth.login_flow.async_configure(
result["flow_id"], {"code": MOCK_CODE}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"].id == "mock-id"
+4 -4
View File
@@ -139,18 +139,18 @@ async def test_login_flow_validates(
"""Test login flow."""
flow = await provider.async_login_flow({})
result = await flow.async_step_init()
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await flow.async_step_init(
{"username": "bad-user", "password": "bad-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await flow.async_step_init(
{"username": "good-user", "password": "good-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"]["username"] == "good-user"
@@ -160,5 +160,5 @@ async def test_strip_username(provider: command_line.CommandLineAuthProvider) ->
result = await flow.async_step_init(
{"username": "\t\ngood-user ", "password": "good-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"]["username"] == "good-user"
+8 -8
View File
@@ -161,24 +161,24 @@ async def test_login_flow_validates(data: hass_auth.Data, hass: HomeAssistant) -
)
flow = await provider.async_login_flow({})
result = await flow.async_step_init()
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await flow.async_step_init(
{"username": "incorrect-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await flow.async_step_init(
{"username": "TEST-user ", "password": "incorrect-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await flow.async_step_init(
{"username": "test-USER", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"]["username"] == "test-USER"
@@ -260,24 +260,24 @@ async def test_legacy_login_flow_validates(
)
flow = await provider.async_login_flow({})
result = await flow.async_step_init()
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
result = await flow.async_step_init(
{"username": "incorrect-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await flow.async_step_init(
{"username": "test-user", "password": "incorrect-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"]["base"] == "invalid_auth"
result = await flow.async_step_init(
{"username": "test-user", "password": "test-pass"}
)
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["data"]["username"] == "test-user"
+15 -15
View File
@@ -172,12 +172,12 @@ async def test_create_new_user(hass: HomeAssistant) -> None:
)
step = await manager.login_flow.async_init(("insecure_example", None))
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
step = await manager.login_flow.async_configure(
step["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
credential = step["result"]
assert credential is not None
@@ -241,12 +241,12 @@ async def test_login_as_existing_user(mock_hass) -> None:
)
step = await manager.login_flow.async_init(("insecure_example", None))
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
step = await manager.login_flow.async_configure(
step["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
credential = step["result"]
user = await manager.async_get_user_by_credentials(credential)
@@ -840,14 +840,14 @@ async def test_login_with_auth_module(mock_hass) -> None:
)
step = await manager.login_flow.async_init(("insecure_example", None))
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
step = await manager.login_flow.async_configure(
step["flow_id"], {"username": "test-user", "password": "test-pass"}
)
# After auth_provider validated, request auth module input form
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
assert step["step_id"] == "mfa"
step = await manager.login_flow.async_configure(
@@ -855,7 +855,7 @@ async def test_login_with_auth_module(mock_hass) -> None:
)
# Invalid code error
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
assert step["step_id"] == "mfa"
assert step["errors"] == {"base": "invalid_code"}
@@ -864,7 +864,7 @@ async def test_login_with_auth_module(mock_hass) -> None:
)
# Finally passed, get credential
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert step["result"]
assert step["result"].id == "mock-id"
@@ -915,21 +915,21 @@ async def test_login_with_multi_auth_module(mock_hass) -> None:
)
step = await manager.login_flow.async_init(("insecure_example", None))
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
step = await manager.login_flow.async_configure(
step["flow_id"], {"username": "test-user", "password": "test-pass"}
)
# After auth_provider validated, request select auth module
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
assert step["step_id"] == "select_mfa_module"
step = await manager.login_flow.async_configure(
step["flow_id"], {"multi_factor_auth_module": "module2"}
)
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
assert step["step_id"] == "mfa"
step = await manager.login_flow.async_configure(
@@ -937,7 +937,7 @@ async def test_login_with_multi_auth_module(mock_hass) -> None:
)
# Finally passed, get credential
assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert step["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert step["result"]
assert step["result"].id == "mock-id"
@@ -983,13 +983,13 @@ async def test_auth_module_expired_session(mock_hass) -> None:
)
step = await manager.login_flow.async_init(("insecure_example", None))
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
step = await manager.login_flow.async_configure(
step["flow_id"], {"username": "test-user", "password": "test-pass"}
)
assert step["type"] == data_entry_flow.FlowResultType.FORM
assert step["type"] is data_entry_flow.FlowResultType.FORM
assert step["step_id"] == "mfa"
with freeze_time(dt_util.utcnow() + MFA_SESSION_EXPIRATION):
@@ -997,7 +997,7 @@ async def test_auth_module_expired_session(mock_hass) -> None:
step["flow_id"], {"pin": "test-pin"}
)
# login flow abort due session timeout
assert step["type"] == data_entry_flow.FlowResultType.ABORT
assert step["type"] is data_entry_flow.FlowResultType.ABORT
assert step["reason"] == "login_expired"
+3 -3
View File
@@ -231,7 +231,7 @@ async def test_reauth_flow_scenario(
data=mock_config_entry.data,
)
assert flow["type"] == FlowResultType.FORM
assert flow["type"] is FlowResultType.FORM
assert flow["step_id"] == REAUTH_STEP
fw_major = int(ap_status_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
@@ -305,7 +305,7 @@ async def test_reauth_flow_scenarios(
data=mock_config_entry.data,
)
assert flow["type"] == FlowResultType.FORM
assert flow["type"] is FlowResultType.FORM
assert flow["step_id"] == REAUTH_STEP
with patch(
@@ -337,7 +337,7 @@ async def test_reauth_flow_scenarios(
user_input={CONF_PASSWORD: NEW_PASSWORD},
)
assert result["type"] == FlowResultType.ABORT
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
+1 -1
View File
@@ -284,7 +284,7 @@ async def test_setup_entry_failure(
result = await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert result is False
assert mock_config_entry.state == state
assert mock_config_entry.state is state
async def test_fetch_airos_data_auth_error(mock_airos_client: MagicMock) -> None:
+1 -1
View File
@@ -138,4 +138,4 @@ async def test_migrate_future_version_returns_false(
await setup_integration(hass, config_entry)
assert config_entry.state == ConfigEntryState.MIGRATION_ERROR
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
@@ -164,7 +164,7 @@ async def test_error_handling(
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == "unknown", result
@@ -189,7 +189,7 @@ async def test_template_error(
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == "unknown", result
@@ -230,7 +230,7 @@ async def test_template_variables(
hass, "hello", None, context, agent_id="conversation.claude_conversation"
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert (
result.response.speech["plain"]["speech"]
== "Okay, let me take care of that for you."
@@ -382,7 +382,7 @@ async def test_function_call(
system_text = " ".join(block["text"] for block in system if "text" in block)
assert "You are a voice assistant for Home Assistant." in system_text
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert (
result.response.speech["plain"]["speech"]
== "I have successfully called the function"
@@ -457,7 +457,7 @@ async def test_function_exception(
agent_id=agent_id,
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert (
result.response.speech["plain"]["speech"]
== "There was an error calling the function"
@@ -638,7 +638,7 @@ async def test_refusal(
agent_id="conversation.claude_conversation",
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == "unknown"
assert (
result.response.speech["plain"]["speech"]
@@ -670,7 +670,7 @@ async def test_stream_wrong_type(
agent_id="conversation.claude_conversation",
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == "unknown"
assert result.response.speech["plain"]["speech"] == "Expected a stream of messages"
@@ -700,7 +700,7 @@ async def test_double_system_messages(
agent_id="conversation.claude_conversation",
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == "unknown"
assert (
result.response.speech["plain"]["speech"]
@@ -42,7 +42,7 @@ async def test_auth_error_handling(
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == "unknown", result
await hass.async_block_till_done()
@@ -86,7 +86,7 @@ async def test_connection_error_handling(
hass, "hello", None, Context(), agent_id="conversation.claude_conversation"
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == "unknown", result
# Check new state
+2 -2
View File
@@ -190,7 +190,7 @@ async def test_device_trigger_reauth_flow(
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
mock_flow_init.assert_called_once()
assert config_entry.state == ConfigEntryState.SETUP_ERROR
assert config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_shutdown(config_entry_data: MappingProxyType[str, Any]) -> None:
@@ -235,4 +235,4 @@ async def test_get_axis_api_errors(
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == state
assert config_entry.state is state
@@ -26,7 +26,7 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ->
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
@@ -34,7 +34,7 @@ async def test_config_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ->
BASE_CONFIG.copy(),
)
assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
assert (
result2["title"]
== "cluster.region.kusto.windows.net / test-database-name (test-table-name)"
@@ -61,7 +61,7 @@ async def test_config_flow_errors(
context={"source": config_entries.SOURCE_USER},
data=None,
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["type"] is data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
# Test error handling with error
@@ -71,7 +71,7 @@ async def test_config_flow_errors(
result["flow_id"],
BASE_CONFIG.copy(),
)
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["type"] is data_entry_flow.FlowResultType.FORM
assert result2["errors"] == {"base": expected}
schema = result2["data_schema"]
@@ -99,7 +99,7 @@ async def test_config_flow_errors(
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["type"] is data_entry_flow.FlowResultType.FORM
# Retest error handling if error is corrected and connection is successful
@@ -112,4 +112,4 @@ async def test_config_flow_errors(
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result3["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
@@ -70,7 +70,7 @@ async def test_config_flow_step_user(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert result1["type"] == FlowResultType.CREATE_ENTRY
assert result1["type"] is FlowResultType.CREATE_ENTRY
assert result1["result"].title == "Office occupied"
assert result1["next_flow"][0] == FlowType.CONFIG_SUBENTRIES_FLOW
@@ -260,7 +260,7 @@ async def test_single_state_observation(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["type"] is FlowResultType.CREATE_ENTRY
entry_id = result["result"].entry_id
sub_flow_id = result["next_flow"][1]
@@ -287,7 +287,7 @@ async def test_single_state_observation(hass: HomeAssistant) -> None:
},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["type"] is FlowResultType.CREATE_ENTRY
await hass.async_block_till_done()
config_entry = hass.config_entries.async_get_entry(entry_id)
@@ -337,7 +337,7 @@ async def test_single_numeric_state_observation(hass: HomeAssistant) -> None:
CONF_PRIOR: 20,
},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["type"] is FlowResultType.CREATE_ENTRY
config_entry = result["result"]
sub_flow_id = result["next_flow"][1]
await hass.async_block_till_done()
@@ -408,7 +408,7 @@ async def test_multi_numeric_state_observation(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["type"] is FlowResultType.CREATE_ENTRY
config_entry = result["result"]
sub_flow_id = result["next_flow"][1]
@@ -546,7 +546,7 @@ async def test_single_template_observation(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["type"] is FlowResultType.CREATE_ENTRY
config_entry = result["result"]
sub_flow_id = result["next_flow"][1]
@@ -1086,7 +1086,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert result.get("errors") is None
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["type"] is FlowResultType.CREATE_ENTRY
config_entry = result["result"]
sub_flow_id = result["next_flow"][1]
-102
View File
@@ -1642,108 +1642,6 @@ async def test_register_callback_by_address(
assert service_info.manufacturer_id == 89
@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_with_scan_interval_registers_active_scan(
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
"""scan_interval + address forwards to habluetooth's active-scan scheduler."""
mock_bt: list[Any] = []
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
):
await async_setup_with_default_adapter(hass)
with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
manager = bluetooth.api._get_manager(hass)
sched = manager._auto_scheduler
address = "44:44:33:11:23:45"
def _cb(_si: BluetoothServiceInfo, _ch: BluetoothChange) -> None:
return None
cancel = bluetooth.async_register_callback(
hass,
_cb,
{"address": address},
BluetoothScanningMode.ACTIVE,
scan_interval=300.0,
scan_duration=5.0,
)
assert address in sched._requests_by_address
request = next(iter(sched._requests_by_address[address]))
assert request.scan_interval == 300.0
assert request.scan_duration == 5.0
cancel()
assert address not in sched._requests_by_address
@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_passive_mode_skips_active_scan(
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
"""PASSIVE mode never registers an active-scan request, even with cadence."""
mock_bt: list[Any] = []
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
):
await async_setup_with_default_adapter(hass)
with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
manager = bluetooth.api._get_manager(hass)
sched = manager._auto_scheduler
def _cb(_si: BluetoothServiceInfo, _ch: BluetoothChange) -> None:
return None
cancel = bluetooth.async_register_callback(
hass,
_cb,
{"address": "44:44:33:11:23:45"},
BluetoothScanningMode.PASSIVE,
scan_interval=300.0,
)
assert sched._requests_by_address == {}
cancel()
@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_without_address_skips_active_scan(
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
) -> None:
"""A scan_interval without an address in the matcher is a no-op for the scheduler."""
mock_bt: list[Any] = []
with patch(
"homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
):
await async_setup_with_default_adapter(hass)
with patch.object(hass.config_entries.flow, "async_init"):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
manager = bluetooth.api._get_manager(hass)
sched = manager._auto_scheduler
def _cb(_si: BluetoothServiceInfo, _ch: BluetoothChange) -> None:
return None
cancel = bluetooth.async_register_callback(
hass,
_cb,
{SERVICE_UUID: "cba20d00-224d-11e6-9fb8-0002a5d5c51b"},
BluetoothScanningMode.ACTIVE,
scan_interval=300.0,
)
assert sched._requests_by_address == {}
cancel()
@pytest.mark.usefixtures("enable_bluetooth")
async def test_register_callback_by_address_connectable_only(
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock
+1 -1
View File
@@ -72,7 +72,7 @@ async def test_init_failure(
"""Test an initialization error on integration load."""
mock_bring_client.login.side_effect = exception
await setup_integration(hass, bring_config_entry)
assert bring_config_entry.state == status
assert bring_config_entry.state is status
assert (
any(
+1 -1
View File
@@ -67,7 +67,7 @@ async def test_client_failure(
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == expected_state
assert config_entry.state is expected_state
flows = hass.config_entries.flow.async_progress()
assert [flow.get("step_id") for flow in flows] == expected_flows
+1 -1
View File
@@ -56,7 +56,7 @@ async def test_async_unload_entry(
result = await hass.config_entries.async_unload(config_entry.entry_id)
assert result is True
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert config_entry.state is ConfigEntryState.NOT_LOADED
async def test_device_info(
+10 -10
View File
@@ -201,7 +201,7 @@ async def test_set_temperature(
{"temperature": {"value": 20}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS
assert err.value.result.no_match_reason is intent.MatchFailedReason.MULTIPLE_TARGETS
# Select by area explicitly (climate_2)
response = await intent.async_handle(
@@ -211,7 +211,7 @@ async def test_set_temperature(
{"area": {"value": bedroom_area.name}, "temperature": {"value": 20.1}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert len(response.matched_states) == 1
assert response.matched_states[0].entity_id == climate_2.entity_id
state = hass.states.get(climate_2.entity_id)
@@ -228,7 +228,7 @@ async def test_set_temperature(
},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.matched_states
assert response.matched_states[0].entity_id == climate_2.entity_id
state = hass.states.get(climate_2.entity_id)
@@ -242,7 +242,7 @@ async def test_set_temperature(
{"floor": {"value": second_floor.name}, "temperature": {"value": 20.3}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.matched_states
assert response.matched_states[0].entity_id == climate_2.entity_id
state = hass.states.get(climate_2.entity_id)
@@ -259,7 +259,7 @@ async def test_set_temperature(
},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.matched_states
assert response.matched_states[0].entity_id == climate_2.entity_id
state = hass.states.get(climate_2.entity_id)
@@ -273,7 +273,7 @@ async def test_set_temperature(
{"name": {"value": "Climate 2"}, "temperature": {"value": 20.5}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert len(response.matched_states) == 1
assert response.matched_states[0].entity_id == climate_2.entity_id
state = hass.states.get(climate_2.entity_id)
@@ -291,7 +291,7 @@ async def test_set_temperature(
# Exception should contain details of what we tried to match
assert isinstance(error.value, intent.MatchFailedError)
assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA
assert error.value.result.no_match_reason is intent.MatchFailedReason.AREA
constraints = error.value.constraints
assert constraints.name is None
assert constraints.area_name == office_area.name
@@ -310,7 +310,7 @@ async def test_set_temperature(
},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS
assert err.value.result.no_match_reason is intent.MatchFailedReason.MULTIPLE_TARGETS
async def test_set_temperature_no_entities(
@@ -330,7 +330,7 @@ async def test_set_temperature_no_entities(
{"temperature": {"value": 20}},
assistant=conversation.DOMAIN,
)
assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN
assert err.value.result.no_match_reason is intent.MatchFailedReason.DOMAIN
async def test_set_temperature_not_supported(hass: HomeAssistant) -> None:
@@ -357,4 +357,4 @@ async def test_set_temperature_not_supported(hass: HomeAssistant) -> None:
# Exception should contain details of what we tried to match
assert isinstance(error.value, intent.MatchFailedError)
assert error.value.result.no_match_reason == intent.MatchFailedReason.FEATURE
assert error.value.result.no_match_reason is intent.MatchFailedReason.FEATURE
+1 -1
View File
@@ -114,4 +114,4 @@ async def test_migrate_future_version_returns_false(
await setup_integration(hass, config_entry)
assert config_entry.state == ConfigEntryState.MIGRATION_ERROR
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
@@ -785,7 +785,7 @@ async def test_get_progress_index(
)
for form in (form_hassio, form_user, form_reconfigure):
assert form["type"] == data_entry_flow.FlowResultType.FORM
assert form["type"] is data_entry_flow.FlowResultType.FORM
assert form["step_id"] == "account"
await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"})
@@ -961,9 +961,9 @@ async def test_get_progress_subscribe(
"test", context=context
)
assert forms["bluetooth"]["type"] == data_entry_flow.FlowResultType.ABORT
assert forms["bluetooth"]["type"] is data_entry_flow.FlowResultType.ABORT
for key in ("hassio", "user", "reauth", "reconfigure"):
assert forms[key]["type"] == data_entry_flow.FlowResultType.FORM
assert forms[key]["type"] is data_entry_flow.FlowResultType.FORM
assert forms[key]["step_id"] == "account"
for key in ("hassio", "user", "reauth", "reconfigure"):
@@ -1100,9 +1100,9 @@ async def test_get_progress_subscribe_in_progress(
"test", context=context
)
assert forms["bluetooth"]["type"] == data_entry_flow.FlowResultType.ABORT
assert forms["bluetooth"]["type"] is data_entry_flow.FlowResultType.ABORT
for key in ("hassio", "user", "reauth", "reconfigure"):
assert forms[key]["type"] == data_entry_flow.FlowResultType.FORM
assert forms[key]["type"] is data_entry_flow.FlowResultType.FORM
assert forms[key]["step_id"] == "account"
await ws_client.send_json({"id": 1, "type": "config_entries/flow/subscribe"})
@@ -1235,16 +1235,16 @@ async def test_get_progress_subscribe_in_progress_bad_flow(
"test", context=context
)
assert forms["bluetooth"]["type"] == data_entry_flow.FlowResultType.ABORT
assert forms["bluetooth"]["type"] is data_entry_flow.FlowResultType.ABORT
for key in ("hassio", "user", "reauth", "reconfigure"):
assert forms[key]["type"] == data_entry_flow.FlowResultType.FORM
assert forms[key]["type"] is data_entry_flow.FlowResultType.FORM
assert forms[key]["step_id"] == "account"
with mock_config_flow("test2", BadFlow):
forms["bad"] = await hass.config_entries.flow.async_init(
"test2", context={"source": core_ce.SOURCE_REAUTH, "entry_id": "1234"}
)
assert forms["bad"]["type"] == data_entry_flow.FlowResultType.FORM
assert forms["bad"]["type"] is data_entry_flow.FlowResultType.FORM
assert forms["bad"]["step_id"] == "account"
await ws_client.send_json({"id": 1, "type": "config_entries/flow/subscribe"})
@@ -116,7 +116,7 @@ async def test_hidden_entities_skipped(
)
assert len(calls) == 0
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -135,13 +135,13 @@ async def test_exposed_domains(hass: HomeAssistant) -> None:
result = await conversation.async_converse(
hass, "unlock front door", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
result = await conversation.async_converse(
hass, "run my script", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -191,7 +191,7 @@ async def test_exposed_areas(
)
# All is well for the exposed kitchen light
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
@@ -202,14 +202,14 @@ async def test_exposed_areas(
)
# This should be an error because the lights in that area are not exposed
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
# But we can still ask questions about the bedroom, even with no exposed entities
result = await conversation.async_converse(
hass, "how many lights are on in the bedroom?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
@pytest.mark.usefixtures("init_components")
@@ -248,7 +248,7 @@ async def test_punctuation(hass: HomeAssistant) -> None:
assert len(calls) == 1
assert calls[0].data["entity_id"][0] == "light.test_light"
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["name"]["value"] == "test light"
assert result.response.intent.slots["name"]["text"] == "test light"
@@ -326,7 +326,7 @@ async def test_unexposed_entities_skipped(
)
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
@@ -338,7 +338,7 @@ async def test_unexposed_entities_skipped(
hass, "how many lights are on in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == exposed_light.entity_id
@@ -403,7 +403,7 @@ async def test_duplicated_names_resolved_with_device_area(
assert len(calls) == 1
assert calls[0].data["entity_id"][0] == bedroom_light.entity_id
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots.get("name", {}).get("value") == name
assert result.response.intent.slots.get("name", {}).get("text") == name
@@ -421,7 +421,7 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None:
unregister = manager.register_trigger(trigger_sentences, callback)
result = await conversation.async_converse(hass, "Not the trigger", None, Context())
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
# Using different case and including punctuation
test_sentences = ["it's party time!", "IT IS TIME TO PARTY."]
@@ -430,7 +430,7 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None:
result = await conversation.async_converse(hass, sentence, None, Context())
assert callback.call_count == 1
assert callback.call_args[0][0].text == sentence
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE, (
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE, (
sentence
)
assert result.response.speech == {
@@ -443,7 +443,7 @@ async def test_trigger_sentences(hass: HomeAssistant) -> None:
callback.reset_mock()
for sentence in test_sentences:
result = await conversation.async_converse(hass, sentence, None, Context())
assert result.response.response_type == intent.IntentResponseType.ERROR, (
assert result.response.response_type is intent.IntentResponseType.ERROR, (
sentence
)
@@ -479,7 +479,7 @@ async def test_trigger_sentence_response_translation(
result = await conversation.async_converse(
hass, "test sentence", None, Context()
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.speech == {
"plain": {"speech": expected, "extra_data": None}
}
@@ -493,7 +493,7 @@ async def test_shopping_list_add_item(hass: HomeAssistant) -> None:
result = await conversation.async_converse(
hass, "add apples to my shopping list", None, Context()
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.speech == {
"plain": {"speech": "Added apples", "extra_data": None}
}
@@ -506,7 +506,7 @@ async def test_nevermind_intent(hass: HomeAssistant) -> None:
assert result.response.intent is not None
assert result.response.intent.intent_type == intent.INTENT_NEVERMIND
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert not result.response.speech
@@ -517,7 +517,7 @@ async def test_respond_intent(hass: HomeAssistant) -> None:
assert result.response.intent is not None
assert result.response.intent.intent_type == intent.INTENT_RESPOND
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.speech["plain"]["speech"] == "Hello from Home Assistant."
@@ -584,7 +584,7 @@ async def test_satellite_area_context(
satellite_id=kitchen_satellite.entity_id,
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_kitchen.id
assert result.response.intent.slots["area"]["text"] == area_kitchen.normalized_name
@@ -608,7 +608,7 @@ async def test_satellite_area_context(
satellite_id=kitchen_satellite.entity_id,
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
@@ -632,7 +632,7 @@ async def test_satellite_area_context(
device_id=bedroom_satellite.id,
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["area"]["value"] == area_bedroom.id
assert result.response.intent.slots["area"]["text"] == area_bedroom.normalized_name
@@ -652,7 +652,7 @@ async def test_satellite_area_context(
hass, f"turn {command} all lights", None, Context(), None
)
await hass.async_block_till_done()
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
# All lights should have been targeted
assert {s.entity_id for s in result.response.matched_states} == {
@@ -667,7 +667,7 @@ async def test_error_no_device(hass: HomeAssistant) -> None:
hass, "turn on missing entity", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -685,7 +685,7 @@ async def test_error_no_device_exposed(hass: HomeAssistant) -> None:
hass, "turn on kitchen light", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -700,7 +700,7 @@ async def test_error_no_area(hass: HomeAssistant) -> None:
hass, "turn on the lights in missing area", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -715,7 +715,7 @@ async def test_error_no_floor(hass: HomeAssistant) -> None:
hass, "turn on all the lights on missing floor", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -734,7 +734,7 @@ async def test_error_no_device_in_area(
hass, "turn on missing entity in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -754,7 +754,7 @@ async def test_error_no_device_on_floor(
hass, "turn on missing entity on ground floor", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -809,7 +809,7 @@ async def test_error_no_device_on_floor_exposed(
hass, "turn on test light on the ground floor", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -848,7 +848,7 @@ async def test_error_no_device_in_area_exposed(
hass, "turn on test light in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -877,7 +877,7 @@ async def test_error_no_domain(hass: HomeAssistant) -> None:
hass, "turn on the fans", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -912,7 +912,7 @@ async def test_error_no_domain_exposed(hass: HomeAssistant) -> None:
hass, "turn on the fans", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -931,7 +931,7 @@ async def test_error_no_domain_in_area(
hass, "turn on the lights in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -967,7 +967,7 @@ async def test_error_no_domain_in_area_exposed(
hass, "turn on the lights in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -991,7 +991,7 @@ async def test_error_no_domain_on_floor(
hass, "turn on all lights on the ground floor", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -1009,7 +1009,7 @@ async def test_error_no_domain_on_floor(
hass, "turn on all lights upstairs", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -1048,7 +1048,7 @@ async def test_error_no_domain_on_floor_exposed(
hass, "turn on all lights on the ground floor", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -1086,7 +1086,7 @@ async def test_error_no_device_class(hass: HomeAssistant) -> None:
hass, "open the windows", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1135,7 +1135,7 @@ async def test_error_no_device_class_exposed(hass: HomeAssistant) -> None:
hass, "open all the windows", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1156,7 +1156,7 @@ async def test_error_no_device_class_in_area(
hass, "open bedroom windows", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -1191,7 +1191,7 @@ async def test_error_no_device_class_in_area_exposed(
hass, "open bedroom windows", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -1246,7 +1246,7 @@ async def test_error_no_device_class_on_floor_exposed(
hass, "open ground floor windows", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1268,7 +1268,7 @@ async def test_error_no_intent(hass: HomeAssistant) -> None:
hass, "do something", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
)
@@ -1305,7 +1305,7 @@ async def test_error_duplicate_names(
result = await conversation.async_converse(
hass, f"turn on {name}", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1319,7 +1319,7 @@ async def test_error_duplicate_names(
result = await conversation.async_converse(
hass, f"is {name} on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1362,7 +1362,7 @@ async def test_duplicate_names_but_one_is_exposed(
result = await conversation.async_converse(
hass, f"turn on {name}", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.matched_states[0].entity_id == kitchen_light_1.entity_id
@@ -1399,7 +1399,7 @@ async def test_error_duplicate_names_same_area(
result = await conversation.async_converse(
hass, f"turn on {name} in {area_kitchen.name}", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1414,7 +1414,7 @@ async def test_error_duplicate_names_same_area(
result = await conversation.async_converse(
hass, f"is {name} on in the {area_kitchen.name}?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1464,7 +1464,7 @@ async def test_duplicate_names_same_area_but_one_is_exposed(
result = await conversation.async_converse(
hass, f"turn on {name} in {area_kitchen.name}", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.matched_states[0].entity_id == kitchen_light_1.entity_id
@@ -1530,20 +1530,20 @@ async def test_duplicate_names_different_areas(
result = await conversation.async_converse(
hass, f"turn on {name}", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
# Target kitchen light by using kitchen device
result = await conversation.async_converse(
hass, f"turn on {name}", None, Context(), None, device_id=device_kitchen.id
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
# Target bedroom light by using bedroom device
result = await conversation.async_converse(
hass, f"turn on {name}", None, Context(), None, device_id=device_bedroom.id
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.matched_states[0].entity_id == bedroom_light.entity_id
@@ -1562,7 +1562,7 @@ async def test_error_wrong_state(hass: HomeAssistant) -> None:
hass, "pause test player", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert result.response.speech["plain"]["speech"] == "Sorry, no device is playing"
@@ -1583,7 +1583,7 @@ async def test_error_feature_not_supported(hass: HomeAssistant) -> None:
hass, "set test player volume to 100%", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -1615,7 +1615,7 @@ async def test_error_no_timer_support(
hass, "set a 5 minute timer", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
assert (
result.response.speech["plain"]["speech"]
@@ -1639,7 +1639,7 @@ async def test_error_timer_not_found(hass: HomeAssistant) -> None:
hass, "pause timer", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
assert (
result.response.speech["plain"]["speech"] == "Sorry, I couldn't find that timer"
@@ -1677,18 +1677,18 @@ async def test_error_multiple_timers_matched(
result = await conversation.async_converse(
hass, "set a timer for 5 minutes", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
result = await conversation.async_converse(
hass, "set a timer for 5 minutes", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
# Cannot target multiple timers
result = await conversation.async_converse(
hass, "cancel timer", None, Context(), None, device_id=device_id
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.FAILED_TO_HANDLE
assert (
result.response.speech["plain"]["speech"]
@@ -1714,7 +1714,7 @@ async def test_no_states_matched_default_error(
hass, "turn on lights in the kitchen", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
@@ -1802,7 +1802,7 @@ async def test_all_domains_loaded(hass: HomeAssistant) -> None:
)
# Invalid target vs. no intent recognized
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
@@ -1856,7 +1856,7 @@ async def test_same_named_entities_in_different_areas(
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert (
result.response.intent.slots.get("name", {}).get("value") == kitchen_light.name
@@ -1876,7 +1876,7 @@ async def test_same_named_entities_in_different_areas(
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert (
result.response.intent.slots.get("name", {}).get("value") == bedroom_light.name
@@ -1892,19 +1892,19 @@ async def test_same_named_entities_in_different_areas(
result = await conversation.async_converse(
hass, "turn on overhead light", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
# Querying a duplicate name should also fail
result = await conversation.async_converse(
hass, "is the overhead light on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
# But we can still ask questions that don't rely on the name
result = await conversation.async_converse(
hass, "how many lights are on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
@pytest.mark.usefixtures("init_components")
@@ -1955,7 +1955,7 @@ async def test_same_aliased_entities_in_different_areas(
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
@@ -1971,7 +1971,7 @@ async def test_same_aliased_entities_in_different_areas(
await hass.async_block_till_done()
assert len(calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
@@ -1983,19 +1983,19 @@ async def test_same_aliased_entities_in_different_areas(
result = await conversation.async_converse(
hass, "turn on overhead light", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
# Querying a duplicate alias should also fail
result = await conversation.async_converse(
hass, "is the overhead light on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
# But we can still ask questions that don't rely on the alias
result = await conversation.async_converse(
hass, "how many lights are on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
@pytest.mark.usefixtures("init_components")
@@ -2027,7 +2027,7 @@ async def test_device_id_in_handler(hass: HomeAssistant) -> None:
Context(),
device_id=device_id,
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert handler.device_id == device_id
@@ -2070,7 +2070,7 @@ async def test_name_wildcard_lower_priority(hass: HomeAssistant) -> None:
result = await conversation.async_converse(
hass, "I'd like to order a stout please", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert beer_handler.triggered
assert not food_handler.triggered
@@ -2079,7 +2079,7 @@ async def test_name_wildcard_lower_priority(hass: HomeAssistant) -> None:
result = await conversation.async_converse(
hass, "I'd like to order a cookie please", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert not beer_handler.triggered
assert food_handler.triggered
@@ -2871,7 +2871,7 @@ async def test_query_same_name_different_areas(
result = await conversation.async_converse(
hass, "is the overhead light on?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
# Succeeds using area from device (kitchen)
result = await conversation.async_converse(
@@ -2882,7 +2882,7 @@ async def test_query_same_name_different_areas(
None,
device_id=kitchen_device.id,
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
assert len(result.response.matched_states) == 1
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
@@ -3059,7 +3059,7 @@ async def test_entities_names_are_not_templates(hass: HomeAssistant) -> None:
)
assert result is not None
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
# Not exposed
expose_entity(hass, "light.test_light", False)
@@ -3072,7 +3072,7 @@ async def test_entities_names_are_not_templates(hass: HomeAssistant) -> None:
)
assert result is not None
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
@pytest.mark.parametrize(
@@ -3339,7 +3339,7 @@ async def test_state_names_are_not_translated(
result = await conversation.async_converse(
hass, "what is the weather like?", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
assert result.response.response_type is intent.IntentResponseType.QUERY_ANSWER
mock_async_render.assert_called_once()
assert (
@@ -3396,7 +3396,7 @@ async def test_intent_tool_call_in_chat_log(hass: HomeAssistant) -> None:
hass, "turn on test light", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
with (
chat_session.async_get_chat_session(hass, result.conversation_id) as session,
@@ -3448,7 +3448,7 @@ async def test_trigger_tool_call_in_chat_log(hass: HomeAssistant) -> None:
hass, trigger_sentence, None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
with (
chat_session.async_get_chat_session(hass, result.conversation_id) as session,
@@ -3486,7 +3486,7 @@ async def test_no_tool_call_on_no_intent_match(hass: HomeAssistant) -> None:
hass, "this is a random sentence that should not match", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
with (
chat_session.async_get_chat_session(hass, result.conversation_id) as session,
@@ -3511,7 +3511,7 @@ async def test_intent_tool_call_with_error_response(hass: HomeAssistant) -> None
hass, "turn on the non existent device", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.response_type is intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
with (
@@ -88,7 +88,7 @@ async def test_cover_set_position(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Opening"
assert len(calls) == 1
call = calls[0]
@@ -102,7 +102,7 @@ async def test_cover_set_position(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Closing"
assert len(calls) == 1
call = calls[0]
@@ -116,7 +116,7 @@ async def test_cover_set_position(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Position set"
assert len(calls) == 1
call = calls[0]
@@ -144,7 +144,7 @@ async def test_cover_device_class(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Opening the garage"
assert len(calls) == 1
call = calls[0]
@@ -168,7 +168,7 @@ async def test_valve_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Opening"
assert len(calls) == 1
call = calls[0]
@@ -182,7 +182,7 @@ async def test_valve_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Closing"
assert len(calls) == 1
call = calls[0]
@@ -196,7 +196,7 @@ async def test_valve_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Position set"
assert len(calls) == 1
call = calls[0]
@@ -229,7 +229,7 @@ async def test_vacuum_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Started"
assert len(calls) == 1
call = calls[0]
@@ -243,7 +243,7 @@ async def test_vacuum_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Returning"
assert len(calls) == 1
call = calls[0]
@@ -275,7 +275,7 @@ async def test_media_player_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Paused"
assert len(calls) == 1
call = calls[0]
@@ -294,7 +294,7 @@ async def test_media_player_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Resumed"
assert len(calls) == 1
call = calls[0]
@@ -313,7 +313,7 @@ async def test_media_player_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Playing next"
assert len(calls) == 1
call = calls[0]
@@ -329,7 +329,7 @@ async def test_media_player_intents(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "Volume set"
assert len(calls) == 1
call = calls[0]
@@ -399,7 +399,7 @@ async def test_turn_floor_lights_on_off(
)
assert len(on_calls) == 2
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert {s.entity_id for s in result.response.matched_states} == {
kitchen_light.entity_id,
living_room_light.entity_id,
@@ -411,7 +411,7 @@ async def test_turn_floor_lights_on_off(
)
assert len(on_calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert {s.entity_id for s in result.response.matched_states} == {
bedroom_light.entity_id
}
@@ -422,7 +422,7 @@ async def test_turn_floor_lights_on_off(
)
assert len(off_calls) == 1
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert {s.entity_id for s in result.response.matched_states} == {
bedroom_light.entity_id
}
@@ -474,7 +474,7 @@ async def test_date_time(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "September 17th, 2013"
result = await conversation.async_converse(
@@ -483,5 +483,5 @@ async def test_date_time(
await hass.async_block_till_done()
response = result.response
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert response.speech["plain"]["speech"] == "1:02 AM"
+1 -1
View File
@@ -57,7 +57,7 @@ async def test_init_failure(
"""Test an initialization error on integration load."""
mock_cookidoo_client.login.side_effect = exception
await setup_integration(hass, cookidoo_config_entry)
assert cookidoo_config_entry.state == status
assert cookidoo_config_entry.state is status
@pytest.mark.parametrize(
+3 -3
View File
@@ -43,7 +43,7 @@ async def test_open_cover_intent(hass: HomeAssistant, slots: dict[str, Any]) ->
)
await hass.async_block_till_done()
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert len(calls) == 1
call = calls[0]
assert call.domain == DOMAIN
@@ -75,7 +75,7 @@ async def test_close_cover_intent(hass: HomeAssistant, slots: dict[str, Any]) ->
)
await hass.async_block_till_done()
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert len(calls) == 1
call = calls[0]
assert call.domain == DOMAIN
@@ -110,7 +110,7 @@ async def test_set_cover_position(hass: HomeAssistant, slots: dict[str, Any]) ->
)
await hass.async_block_till_done()
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert len(calls) == 1
call = calls[0]
assert call.domain == DOMAIN
+1 -1
View File
@@ -29,7 +29,7 @@ async def test_set_speed_intent(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.response_type is intent.IntentResponseType.ACTION_DONE
assert len(calls) == 1
call = calls[0]
assert call.domain == DOMAIN
+1 -1
View File
@@ -35,4 +35,4 @@ async def test_setup_exceptions(
"""Test the _async_setup."""
mock_firefly_client.get_about.side_effect = exception
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state == expected_state
assert mock_config_entry.state is expected_state
+3 -3
View File
@@ -679,7 +679,7 @@ async def test_setup_with_retryable_setup_entry_error_custom_server(
await hass.async_block_till_done(wait_background_tasks=True)
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
assert config_entries[0].state == expected_config_entry_state
assert config_entries[0].state is expected_config_entry_state
assert expected_log_message in caplog.text
@@ -716,7 +716,7 @@ async def test_setup_with_retryable_setup_entry_error_default_server(
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == has_go2rtc_entry
for config_entry in config_entries:
assert config_entry.state == expected_config_entry_state
assert config_entry.state is expected_config_entry_state
assert expected_log_message in caplog.text
@@ -750,7 +750,7 @@ async def test_setup_with_version_error(
await hass.async_block_till_done(wait_background_tasks=True)
config_entries = hass.config_entries.async_entries(DOMAIN)
assert len(config_entries) == 1
assert config_entries[0].state == expected_config_entry_state
assert config_entries[0].state is expected_config_entry_state
assert expected_log_message in caplog.text
@@ -74,7 +74,7 @@ async def test_error_handling(
Context(),
agent_id="conversation.google_ai_conversation",
)
assert result.response.response_type == intent.IntentResponseType.ERROR, result
assert result.response.response_type is intent.IntentResponseType.ERROR, result
assert result.response.error_code == "unknown", result
assert (
result.response.as_dict()["speech"]["plain"]["speech"] == ERROR_GETTING_RESPONSE
@@ -249,7 +249,7 @@ async def test_function_call(
agent_id=agent_id,
device_id="test_device",
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert (
result.response.as_dict()["speech"]["plain"]["speech"]
== "I've called the test function with the provided parameters."
@@ -356,7 +356,7 @@ async def test_google_search_tool_is_sent(
agent_id=agent_id,
device_id="test_device",
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.response_type is intent.IntentResponseType.ACTION_DONE
assert (
result.response.as_dict()["speech"]["plain"]["speech"]
== "The last winner of the 2024 FIFA World Cup was Argentina."
@@ -406,7 +406,7 @@ async def test_blocked_response(
device_id="test_device",
)
assert result.response.response_type == intent.IntentResponseType.ERROR, result
assert result.response.response_type is intent.IntentResponseType.ERROR, result
assert result.response.error_code == "unknown", result
assert result.response.as_dict()["speech"]["plain"]["speech"] == (
"The message got blocked due to content violations, reason: SAFETY"
@@ -450,7 +450,7 @@ async def test_empty_response(
agent_id=agent_id,
device_id="test_device",
)
assert result.response.response_type == intent.IntentResponseType.ERROR, result
assert result.response.response_type is intent.IntentResponseType.ERROR, result
assert result.response.error_code == "unknown", result
assert result.response.as_dict()["speech"]["plain"]["speech"] == (
"Unable to get response"
@@ -485,7 +485,7 @@ async def test_none_response(
device_id="test_device",
)
assert result.response.response_type == intent.IntentResponseType.ERROR, result
assert result.response.response_type is intent.IntentResponseType.ERROR, result
assert result.response.error_code == "unknown", result
assert result.response.as_dict()["speech"]["plain"]["speech"] == (
"The message got blocked due to content violations, reason: unknown"
@@ -514,7 +514,7 @@ async def test_converse_error(
agent_id="conversation.google_ai_conversation",
)
assert result.response.response_type == intent.IntentResponseType.ERROR, result
assert result.response.response_type is intent.IntentResponseType.ERROR, result
assert result.response.error_code == "unknown", result
assert result.response.as_dict()["speech"]["plain"]["speech"] == (
"Error preparing LLM API"
+2 -2
View File
@@ -178,7 +178,7 @@ async def test_token_refresh_error(
assert not await integration_setup(client)
await hass.async_block_till_done()
assert config_entry.state == expected_config_entry_state
assert config_entry.state is expected_config_entry_state
@pytest.mark.parametrize(
@@ -199,7 +199,7 @@ async def test_client_error(
client_with_exception.get_home_appliances.return_value = None
client_with_exception.get_home_appliances.side_effect = exception
assert not await integration_setup(client_with_exception)
assert config_entry.state == expected_state
assert config_entry.state is expected_state
assert client_with_exception.get_home_appliances.call_count == 1
@@ -357,7 +357,7 @@ async def consume_progress_flow(
result = await hass.config_entries.flow.async_configure(flow_id)
flow_id = result["flow_id"]
if result["type"] != FlowResultType.SHOW_PROGRESS:
if result["type"] is not FlowResultType.SHOW_PROGRESS:
break
assert result["type"] is FlowResultType.SHOW_PROGRESS

Some files were not shown because too many files have changed in this diff Show More