From e04ecc69120553c6caaf7ba78fe7baed6404d21f Mon Sep 17 00:00:00 2001 From: Manu Date: Sun, 13 Sep 2026 11:40:50 +0200 Subject: [PATCH 01/19] Move HTTP views into their own module in HTML5 integration (#181889) --- homeassistant/components/html5/const.py | 2 + homeassistant/components/html5/http.py | 302 +++++++++++++++++++++++ homeassistant/components/html5/notify.py | 280 +-------------------- tests/components/html5/conftest.py | 12 + tests/components/html5/test_event.py | 4 +- tests/components/html5/test_notify.py | 137 +++++----- 6 files changed, 387 insertions(+), 350 deletions(-) create mode 100644 homeassistant/components/html5/http.py diff --git a/homeassistant/components/html5/const.py b/homeassistant/components/html5/const.py index dd447b0e4c1b50..08f8bbe266d880 100644 --- a/homeassistant/components/html5/const.py +++ b/homeassistant/components/html5/const.py @@ -25,3 +25,5 @@ ATTR_TTL = "ttl" ATTR_URGENCY = "urgency" ATTR_VIBRATE = "vibrate" +ATTR_SUBSCRIPTION = "subscription" +ATTR_ENDPOINT = "endpoint" diff --git a/homeassistant/components/html5/http.py b/homeassistant/components/html5/http.py new file mode 100644 index 00000000000000..e24a1b9213e908 --- /dev/null +++ b/homeassistant/components/html5/http.py @@ -0,0 +1,302 @@ +"""HTTP views for the HTML5 integration.""" + +from contextlib import suppress +from http import HTTPStatus +import logging +from typing import Any, cast +import warnings + +from aiohttp import web +from aiohttp.hdrs import AUTHORIZATION +import jwt +from jwt.warnings import InsecureKeyLengthWarning +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.components.notify import ATTR_DATA, ATTR_TARGET +from homeassistant.const import ATTR_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.json import save_json +from homeassistant.util import ensure_unique_string + +from .const import ATTR_ACTION, ATTR_ENDPOINT, ATTR_SUBSCRIPTION, ATTR_TAG, DOMAIN +from .entity import Registration +from .issue import deprecated_event_bus + +_LOGGER = logging.getLogger(__name__) + + +ATTR_TYPE = "type" +ATTR_BROWSER = "browser" +ATTR_KEYS = "keys" +ATTR_AUTH = "auth" +ATTR_P256DH = "p256dh" +ATTR_EXPIRATIONTIME = "expirationTime" +NOTIFY_CALLBACK_EVENT = "html5_notification" + + +KEYS_SCHEMA = vol.All( + dict, + vol.Schema( + { + vol.Required(ATTR_AUTH): cv.string, + vol.Required(ATTR_P256DH): cv.string, + } + ), +) + +SUBSCRIPTION_SCHEMA = vol.All( + dict, + vol.Schema( + { + vol.Required(ATTR_ENDPOINT): vol.Url(), + vol.Required(ATTR_KEYS): KEYS_SCHEMA, + vol.Optional(ATTR_EXPIRATIONTIME): vol.Any(None, cv.positive_int), + } + ), +) + +REGISTER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA, + vol.Required(ATTR_BROWSER): vol.In(["chrome", "firefox"]), + vol.Optional(ATTR_NAME): cv.string, + } +) + +CALLBACK_EVENT_PAYLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_TAG): cv.string, + vol.Required(ATTR_TYPE): vol.In(["received", "clicked", "closed"]), + vol.Required(ATTR_TARGET): cv.string, + vol.Optional(ATTR_ACTION): cv.string, + vol.Optional(ATTR_DATA): dict, + } +) + + +@callback +def async_register_http_views( + hass: HomeAssistant, json_path: str, registrations: dict[str, Registration] +) -> None: + """Register the http views.""" + + hass.http.register_view(HTML5PushRegistrationView(registrations, json_path)) + hass.http.register_view(HTML5PushCallbackView(registrations)) + + +class HTML5PushRegistrationView(HomeAssistantView): + """Accepts push registrations from a browser.""" + + url = "/api/notify.html5" + name = "api:notify.html5" + + def __init__(self, registrations: dict[str, Registration], json_path: str) -> None: + """Init HTML5PushRegistrationView.""" + self.registrations = registrations + self.json_path = json_path + + async def post(self, request: web.Request) -> web.Response: + """Accept the POST request for push registrations from a browser.""" + + try: + data: Registration = await request.json() + except ValueError: + return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) + try: + data = cast(Registration, REGISTER_SCHEMA(data)) + except vol.Invalid as ex: + return self.json_message(humanize_error(data, ex), HTTPStatus.BAD_REQUEST) + + devname = data.get(ATTR_NAME) + data.pop(ATTR_NAME, None) + + name = self.find_registration_name(data, devname) + previous_registration = self.registrations.get(name) + + self.registrations[name] = data + hass = request.app[KEY_HASS] + + try: + await hass.async_add_executor_job( + save_json, self.json_path, self.registrations + ) + except HomeAssistantError: + if previous_registration is not None: + self.registrations[name] = previous_registration + else: + self.registrations.pop(name) + + return self.json_message( + "Error saving registration.", HTTPStatus.INTERNAL_SERVER_ERROR + ) + + return self.json_message("Push notification subscriber registered.") + + def find_registration_name( + self, + data: Registration, + suggested: str | None = None, + ): + """Find a registration name matching data or generate a unique one.""" + endpoint = data["subscription"]["endpoint"] + for key, registration in self.registrations.items(): + subscription = registration["subscription"] + if subscription.get(ATTR_ENDPOINT) == endpoint: + return key + return ensure_unique_string(suggested or "unnamed device", self.registrations) + + async def delete(self, request: web.Request): + """Delete a registration.""" + try: + data: dict[str, Any] = await request.json() + except ValueError: + return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) + + subscription: dict[str, Any] = data[ATTR_SUBSCRIPTION] + + found = None + + for key, registration in self.registrations.items(): + if registration["subscription"] == subscription: + found = key + break + + if not found: + # If not found, unregistering was already done. Return 200 + return self.json_message("Registration not found.") + + reg = self.registrations.pop(found) + hass = request.app[KEY_HASS] + + try: + await hass.async_add_executor_job( + save_json, self.json_path, self.registrations + ) + except HomeAssistantError: + self.registrations[found] = reg + return self.json_message( + "Error saving registration.", HTTPStatus.INTERNAL_SERVER_ERROR + ) + + return self.json_message("Push notification subscriber unregistered.") + + +class HTML5PushCallbackView(HomeAssistantView): + """Accepts push registrations from a browser.""" + + requires_auth = False + url = "/api/notify.html5/callback" + name = "api:notify.html5/callback" + + def __init__(self, registrations: dict[str, Registration]) -> None: + """Init HTML5PushCallbackView.""" + self.registrations = registrations + + def decode_jwt(self, token: str) -> web.Response | dict[str, Any]: + """Find the registration that signed this JWT and return it.""" + + # 1. Check claims w/o verifying to see if a target is in there. + # 2. If target in claims, attempt to verify against the given name. + # 2a. If decode is successful, return the payload. + # 2b. If decode is unsuccessful, return a 401. + + target_check: dict[str, Any] = jwt.decode( + token, algorithms=["ES256", "HS256"], options={"verify_signature": False} + ) + if target_check.get(ATTR_TARGET) in self.registrations: + possible_target = self.registrations[target_check[ATTR_TARGET]] + key = possible_target["subscription"]["keys"]["auth"] + with ( + suppress(jwt.exceptions.DecodeError, jwt.exceptions.InvalidKeyError), + warnings.catch_warnings(), + ): + warnings.simplefilter("ignore", InsecureKeyLengthWarning) + return jwt.decode(token, key, algorithms=["ES256", "HS256"]) + + return self.json_message( + "No target found in JWT", status_code=HTTPStatus.UNAUTHORIZED + ) + + # The following is based on code from Auth0 + # https://auth0.com/docs/quickstart/backend/python + def check_authorization_header( + self, request: web.Request + ) -> web.Response | dict[str, Any]: + """Check the authorization header.""" + if not (auth := request.headers.get(AUTHORIZATION)): + return self.json_message( + "Authorization header is expected", status_code=HTTPStatus.UNAUTHORIZED + ) + + parts = auth.split() + + if parts[0].lower() != "bearer": + return self.json_message( + "Authorization header must start with Bearer", + status_code=HTTPStatus.UNAUTHORIZED, + ) + if len(parts) != 2: + return self.json_message( + "Authorization header must be Bearer token", + status_code=HTTPStatus.UNAUTHORIZED, + ) + + token = parts[1] + try: + payload = self.decode_jwt(token) + except jwt.exceptions.InvalidTokenError: + return self.json_message( + "token is invalid", status_code=HTTPStatus.UNAUTHORIZED + ) + return payload + + async def post(self, request: web.Request) -> web.Response: + """Accept the POST request for push registrations event callback.""" + auth_check = self.check_authorization_header(request) + if not isinstance(auth_check, dict): + return auth_check + + try: + data: dict[str, str] = await request.json() + except ValueError: + return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) + + event_payload: dict[str, Any] = { + ATTR_TAG: data.get(ATTR_TAG), + ATTR_TYPE: data[ATTR_TYPE], + ATTR_TARGET: auth_check[ATTR_TARGET], + } + + if data.get(ATTR_ACTION) is not None: + event_payload[ATTR_ACTION] = data.get(ATTR_ACTION) + + if data.get(ATTR_DATA) is not None: + event_payload[ATTR_DATA] = data.get(ATTR_DATA) + + try: + event_payload = CALLBACK_EVENT_PAYLOAD_SCHEMA(event_payload) + except vol.Invalid as ex: + _LOGGER.warning( + "Callback event payload is not valid: %s", + humanize_error(event_payload, ex), + ) + + event_name = f"{NOTIFY_CALLBACK_EVENT}.{event_payload[ATTR_TYPE]}" + hass = request.app[KEY_HASS] + hass.bus.fire(event_name, event_payload) + async_dispatcher_send( + hass, + DOMAIN, + event_payload[ATTR_TARGET], + event_payload[ATTR_TYPE], + event_payload, + ) + + deprecated_event_bus(hass, event_name) + + return self.json({"status": "ok", "event": event_payload[ATTR_TYPE]}) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index f56c4facb4cb20..3b6148af46e73e 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -11,17 +11,14 @@ import uuid import warnings -from aiohttp import ClientError, ClientResponse, ClientSession, web -from aiohttp.hdrs import AUTHORIZATION +from aiohttp import ClientError, ClientResponse, ClientSession import jwt from jwt.warnings import InsecureKeyLengthWarning from py_vapid import Vapid from pywebpush import WebPusher, WebPushException, webpush_async import voluptuous as vol -from voluptuous.humanize import humanize_error from homeassistant.components import websocket_api -from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -33,20 +30,17 @@ ) from homeassistant.components.websocket_api import ActiveConnection from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME, URL_ROOT +from homeassistant.const import URL_ROOT from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.json import save_json from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import ensure_unique_string from homeassistant.util.json import load_json_object from .const import ( - ATTR_ACTION, ATTR_ACTIONS, ATTR_REQUIRE_INTERACTION, ATTR_TAG, @@ -60,25 +54,12 @@ SERVICE_DISMISS, ) from .entity import HTML5Entity, Registration -from .issue import ( - deprecated_dismiss_action_call, - deprecated_event_bus, - deprecated_notify_action_call, -) +from .http import REGISTER_SCHEMA, async_register_http_views +from .issue import deprecated_dismiss_action_call, deprecated_notify_action_call _LOGGER = logging.getLogger(__name__) -ATTR_SUBSCRIPTION = "subscription" -ATTR_BROWSER = "browser" - -ATTR_ENDPOINT = "endpoint" -ATTR_KEYS = "keys" -ATTR_AUTH = "auth" -ATTR_P256DH = "p256dh" -ATTR_EXPIRATIONTIME = "expirationTime" - -ATTR_TYPE = "type" ATTR_URL = "url" ATTR_DISMISS = "dismiss" ATTR_PRIORITY = "priority" @@ -100,23 +81,6 @@ JWT_VALID_DAYS = 7 VAPID_CLAIM_VALID_HOURS = 12 -KEYS_SCHEMA = vol.All( - dict, - vol.Schema( - {vol.Required(ATTR_AUTH): cv.string, vol.Required(ATTR_P256DH): cv.string} - ), -) - -SUBSCRIPTION_SCHEMA = vol.All( - dict, - vol.Schema( - { - vol.Required(ATTR_ENDPOINT): vol.Url(), - vol.Required(ATTR_KEYS): KEYS_SCHEMA, - vol.Optional(ATTR_EXPIRATIONTIME): vol.Any(None, cv.positive_int), - } - ), -) DISMISS_SERVICE_SCHEMA = vol.Schema( { @@ -125,25 +89,6 @@ } ) -REGISTER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA, - vol.Required(ATTR_BROWSER): vol.In(["chrome", "firefox"]), - vol.Optional(ATTR_NAME): cv.string, - } -) - -CALLBACK_EVENT_PAYLOAD_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TAG): cv.string, - vol.Required(ATTR_TYPE): vol.In(["received", "clicked", "closed"]), - vol.Required(ATTR_TARGET): cv.string, - vol.Optional(ATTR_ACTION): cv.string, - vol.Optional(ATTR_DATA): dict, - } -) - -NOTIFY_CALLBACK_EVENT = "html5_notification" # Badge and timestamp are Chrome specific (not in official spec) HTML5_SHOWNOTIFICATION_PARAMETERS = ( @@ -192,8 +137,7 @@ def websocket_appkey( hass, WS_TYPE_APPKEY, websocket_appkey, SCHEMA_WS_APPKEY ) - hass.http.register_view(HTML5PushRegistrationView(registrations, json_path)) - hass.http.register_view(HTML5PushCallbackView(registrations)) + async_register_http_views(hass, json_path, registrations) session = async_get_clientsession(hass) return HTML5NotificationService( @@ -208,220 +152,6 @@ def _load_config(filename: str) -> dict[str, Registration]: return {} -class HTML5PushRegistrationView(HomeAssistantView): - """Accepts push registrations from a browser.""" - - url = "/api/notify.html5" - name = "api:notify.html5" - - def __init__(self, registrations: dict[str, Registration], json_path: str) -> None: - """Init HTML5PushRegistrationView.""" - self.registrations = registrations - self.json_path = json_path - - async def post(self, request: web.Request) -> web.Response: - """Accept the POST request for push registrations from a browser.""" - - try: - data: Registration = await request.json() - except ValueError: - return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) - try: - data = cast(Registration, REGISTER_SCHEMA(data)) - except vol.Invalid as ex: - return self.json_message(humanize_error(data, ex), HTTPStatus.BAD_REQUEST) - - devname = data.get(ATTR_NAME) - data.pop(ATTR_NAME, None) - - name = self.find_registration_name(data, devname) - previous_registration = self.registrations.get(name) - - self.registrations[name] = data - - try: - hass = request.app[KEY_HASS] - - await hass.async_add_executor_job( - save_json, self.json_path, self.registrations - ) - return self.json_message("Push notification subscriber registered.") - except HomeAssistantError: - if previous_registration is not None: - self.registrations[name] = previous_registration - else: - self.registrations.pop(name) - - return self.json_message( - "Error saving registration.", HTTPStatus.INTERNAL_SERVER_ERROR - ) - - def find_registration_name( - self, - data: Registration, - suggested: str | None = None, - ): - """Find a registration name matching data or generate a unique one.""" - endpoint = data["subscription"]["endpoint"] - for key, registration in self.registrations.items(): - subscription = registration["subscription"] - if subscription.get(ATTR_ENDPOINT) == endpoint: - return key - return ensure_unique_string(suggested or "unnamed device", self.registrations) - - async def delete(self, request: web.Request): - """Delete a registration.""" - try: - data: dict[str, Any] = await request.json() - except ValueError: - return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) - - subscription: dict[str, Any] = data[ATTR_SUBSCRIPTION] - - found = None - - for key, registration in self.registrations.items(): - if registration["subscription"] == subscription: - found = key - break - - if not found: - # If not found, unregistering was already done. Return 200 - return self.json_message("Registration not found.") - - reg = self.registrations.pop(found) - - try: - hass = request.app[KEY_HASS] - - await hass.async_add_executor_job( - save_json, self.json_path, self.registrations - ) - except HomeAssistantError: - self.registrations[found] = reg - return self.json_message( - "Error saving registration.", HTTPStatus.INTERNAL_SERVER_ERROR - ) - - return self.json_message("Push notification subscriber unregistered.") - - -class HTML5PushCallbackView(HomeAssistantView): - """Accepts push registrations from a browser.""" - - requires_auth = False - url = "/api/notify.html5/callback" - name = "api:notify.html5/callback" - - def __init__(self, registrations: dict[str, Registration]) -> None: - """Init HTML5PushCallbackView.""" - self.registrations = registrations - - def decode_jwt(self, token: str) -> web.Response | dict[str, Any]: - """Find the registration that signed this JWT and return it.""" - - # 1. Check claims w/o verifying to see if a target is in there. - # 2. If target in claims, attempt to verify against the given name. - # 2a. If decode is successful, return the payload. - # 2b. If decode is unsuccessful, return a 401. - - target_check: dict[str, Any] = jwt.decode( - token, algorithms=["ES256", "HS256"], options={"verify_signature": False} - ) - if target_check.get(ATTR_TARGET) in self.registrations: - possible_target = self.registrations[target_check[ATTR_TARGET]] - key = possible_target["subscription"]["keys"]["auth"] - with ( - suppress(jwt.exceptions.DecodeError, jwt.exceptions.InvalidKeyError), - warnings.catch_warnings(), - ): - warnings.simplefilter("ignore", InsecureKeyLengthWarning) - return jwt.decode(token, key, algorithms=["ES256", "HS256"]) - - return self.json_message( - "No target found in JWT", status_code=HTTPStatus.UNAUTHORIZED - ) - - # The following is based on code from Auth0 - # https://auth0.com/docs/quickstart/backend/python - def check_authorization_header( - self, request: web.Request - ) -> web.Response | dict[str, Any]: - """Check the authorization header.""" - if not (auth := request.headers.get(AUTHORIZATION)): - return self.json_message( - "Authorization header is expected", status_code=HTTPStatus.UNAUTHORIZED - ) - - parts = auth.split() - - if parts[0].lower() != "bearer": - return self.json_message( - "Authorization header must start with Bearer", - status_code=HTTPStatus.UNAUTHORIZED, - ) - if len(parts) != 2: - return self.json_message( - "Authorization header must be Bearer token", - status_code=HTTPStatus.UNAUTHORIZED, - ) - - token = parts[1] - try: - payload = self.decode_jwt(token) - except jwt.exceptions.InvalidTokenError: - return self.json_message( - "token is invalid", status_code=HTTPStatus.UNAUTHORIZED - ) - return payload - - async def post(self, request: web.Request) -> web.Response: - """Accept the POST request for push registrations event callback.""" - auth_check = self.check_authorization_header(request) - if not isinstance(auth_check, dict): - return auth_check - - try: - data: dict[str, str] = await request.json() - except ValueError: - return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) - - event_payload: dict[str, Any] = { - ATTR_TAG: data.get(ATTR_TAG), - ATTR_TYPE: data[ATTR_TYPE], - ATTR_TARGET: auth_check[ATTR_TARGET], - } - - if data.get(ATTR_ACTION) is not None: - event_payload[ATTR_ACTION] = data.get(ATTR_ACTION) - - if data.get(ATTR_DATA) is not None: - event_payload[ATTR_DATA] = data.get(ATTR_DATA) - - try: - event_payload = CALLBACK_EVENT_PAYLOAD_SCHEMA(event_payload) - except vol.Invalid as ex: - _LOGGER.warning( - "Callback event payload is not valid: %s", - humanize_error(event_payload, ex), - ) - - event_name = f"{NOTIFY_CALLBACK_EVENT}.{event_payload[ATTR_TYPE]}" - hass = request.app[KEY_HASS] - hass.bus.fire(event_name, event_payload) - async_dispatcher_send( - hass, - DOMAIN, - event_payload[ATTR_TARGET], - event_payload[ATTR_TYPE], - event_payload, - ) - - deprecated_event_bus(hass, event_name) - - return self.json({"status": "ok", "event": event_payload[ATTR_TYPE]}) - - class HTML5NotificationService(BaseNotificationService): """Implement the notification service for HTML5.""" diff --git a/tests/components/html5/conftest.py b/tests/components/html5/conftest.py index b818dbe6b7e383..5b79424a1dbbef 100644 --- a/tests/components/html5/conftest.py +++ b/tests/components/html5/conftest.py @@ -91,6 +91,7 @@ def mock_jwt() -> Generator[MagicMock]: with ( patch("homeassistant.components.html5.notify.jwt") as mock_client, + patch("homeassistant.components.html5.http.jwt", new=mock_client), ): mock_client.encode.return_value = "JWT" mock_client.decode.return_value = {"target": "device"} @@ -123,3 +124,14 @@ def mock_vapid() -> Generator[MagicMock]: "priority": "normal", } yield mock_client + + +@pytest.fixture +def mock_save() -> Generator[MagicMock]: + """Mock save_json.""" + + with ( + patch("homeassistant.components.html5.http.save_json") as mock_client, + patch("homeassistant.components.html5.notify.save_json", new=mock_client), + ): + yield mock_client diff --git a/tests/components/html5/test_event.py b/tests/components/html5/test_event.py index 25e783df5e6b46..a97689b64dc067 100644 --- a/tests/components/html5/test_event.py +++ b/tests/components/html5/test_event.py @@ -9,8 +9,8 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.html5.const import DOMAIN -from homeassistant.components.html5.notify import ATTR_ACTION, ATTR_TAG, ATTR_TYPE +from homeassistant.components.html5.const import ATTR_ACTION, ATTR_TAG, DOMAIN +from homeassistant.components.html5.http import ATTR_TYPE from homeassistant.components.notify import ATTR_DATA, ATTR_TARGET from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNKNOWN, Platform diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 564aa3fc92f61c..77320ab8c2ebc6 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -31,7 +31,8 @@ ATTR_VIBRATE, SERVICE_DISMISS, ) -from homeassistant.components.html5.notify import ATTR_ACTION, ATTR_DISMISS, DEFAULT_TTL +from homeassistant.components.html5.http import ATTR_ACTION +from homeassistant.components.html5.notify import ATTR_DISMISS, DEFAULT_TTL from homeassistant.components.html5.services import SERVICE_DISMISS_MESSAGE from homeassistant.components.notify import ( ATTR_DATA, @@ -347,6 +348,7 @@ async def test_registering_new_device_view( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test that the HTML view works.""" await async_setup_component(hass, "http", {}) @@ -359,8 +361,7 @@ async def test_registering_new_device_view( client = await hass_client() - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) + resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) assert resp.status == HTTPStatus.OK assert len(mock_save.mock_calls) == 1 @@ -372,6 +373,7 @@ async def test_registering_new_device_view_with_name( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test that the HTML view works with name attribute.""" await async_setup_component(hass, "http", {}) @@ -387,8 +389,7 @@ async def test_registering_new_device_view_with_name( SUB_WITH_NAME = SUBSCRIPTION_1.copy() SUB_WITH_NAME["name"] = "test device" - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - resp = await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) + resp = await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) assert resp.status == HTTPStatus.OK assert len(mock_save.mock_calls) == 1 @@ -400,6 +401,7 @@ async def test_registering_new_device_expiration_view( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test that the HTML view works.""" await async_setup_component(hass, "http", {}) @@ -412,8 +414,7 @@ async def test_registering_new_device_expiration_view( client = await hass_client() - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) + resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) assert resp.status == HTTPStatus.OK assert mock_save.mock_calls[0][1][1] == {"unnamed device": SUBSCRIPTION_4} @@ -424,6 +425,7 @@ async def test_registering_new_device_fails_view( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test subs. are not altered when registering a new device fails.""" await async_setup_component(hass, "http", {}) @@ -435,11 +437,9 @@ async def test_registering_new_device_fails_view( assert config_entry.state is ConfigEntryState.LOADED client = await hass_client() - with patch( - "homeassistant.components.html5.notify.save_json", - side_effect=HomeAssistantError(), - ): - resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) + mock_save.side_effect = (HomeAssistantError(),) + + resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR @@ -449,6 +449,7 @@ async def test_registering_existing_device_view( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test subscription is updated when registering existing device.""" await async_setup_component(hass, "http", {}) @@ -461,9 +462,8 @@ async def test_registering_existing_device_view( client = await hass_client() - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) - resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) + await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) + resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) assert resp.status == HTTPStatus.OK mock_save.assert_called_with( @@ -476,6 +476,7 @@ async def test_registering_existing_device_view_with_name( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test subscription is updated when reg'ing existing device with name.""" await async_setup_component(hass, "http", {}) @@ -491,9 +492,8 @@ async def test_registering_existing_device_view_with_name( SUB_WITH_NAME = SUBSCRIPTION_1.copy() SUB_WITH_NAME["name"] = "test device" - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) - resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) + await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) + resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) assert resp.status == HTTPStatus.OK @@ -507,6 +507,7 @@ async def test_registering_existing_device_fails_view( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test sub. is not updated when registering existing device fails.""" await async_setup_component(hass, "http", {}) @@ -519,10 +520,9 @@ async def test_registering_existing_device_fails_view( client = await hass_client() - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) - mock_save.side_effect = HomeAssistantError - resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) + await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) + mock_save.side_effect = HomeAssistantError + resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR @@ -532,6 +532,7 @@ async def test_registering_new_device_validation( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test various errors when registering a new device.""" await async_setup_component(hass, "http", {}) @@ -553,11 +554,11 @@ async def test_registering_new_device_validation( resp = await client.post(REGISTER_URL, data=json.dumps({"browser": "chrome"})) assert resp.status == HTTPStatus.BAD_REQUEST - with patch("homeassistant.components.html5.notify.save_json", return_value=False): - resp = await client.post( - REGISTER_URL, - data=json.dumps({"browser": "chrome", "subscription": "sub info"}), - ) + mock_save.return_value = False + resp = await client.post( + REGISTER_URL, + data=json.dumps({"browser": "chrome", "subscription": "sub info"}), + ) assert resp.status == HTTPStatus.BAD_REQUEST @@ -566,6 +567,7 @@ async def test_unregistering_device_view( hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, load_config: MagicMock, + mock_save: MagicMock, ) -> None: """Test that the HTML unregister view works.""" load_config.return_value = { @@ -582,11 +584,10 @@ async def test_unregistering_device_view( client = await hass_client() - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - resp = await client.delete( - REGISTER_URL, - data=json.dumps({"subscription": SUBSCRIPTION_1["subscription"]}), - ) + resp = await client.delete( + REGISTER_URL, + data=json.dumps({"subscription": SUBSCRIPTION_1["subscription"]}), + ) assert resp.status == HTTPStatus.OK assert len(mock_save.mock_calls) == 1 @@ -600,6 +601,7 @@ async def test_unregister_device_view_handle_unknown_subscription( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, + mock_save: MagicMock, ) -> None: """Test that the HTML unregister view handles unknown subscriptions.""" await async_setup_component(hass, "http", {}) @@ -612,11 +614,10 @@ async def test_unregister_device_view_handle_unknown_subscription( client = await hass_client() - with patch("homeassistant.components.html5.notify.save_json") as mock_save: - resp = await client.delete( - REGISTER_URL, - data=json.dumps({"subscription": SUBSCRIPTION_3["subscription"]}), - ) + resp = await client.delete( + REGISTER_URL, + data=json.dumps({"subscription": SUBSCRIPTION_3["subscription"]}), + ) assert resp.status == HTTPStatus.OK, resp.response assert len(mock_save.mock_calls) == 0 @@ -627,6 +628,7 @@ async def test_unregistering_device_view_handles_save_error( hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, load_config: MagicMock, + mock_save: MagicMock, ) -> None: """Test that the HTML unregister view handles save errors.""" load_config.return_value = { @@ -643,14 +645,12 @@ async def test_unregistering_device_view_handles_save_error( client = await hass_client() - with patch( - "homeassistant.components.html5.notify.save_json", - side_effect=HomeAssistantError(), - ): - resp = await client.delete( - REGISTER_URL, - data=json.dumps({"subscription": SUBSCRIPTION_1["subscription"]}), - ) + mock_save.side_effect = HomeAssistantError() + + resp = await client.delete( + REGISTER_URL, + data=json.dumps({"subscription": SUBSCRIPTION_1["subscription"]}), + ) assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR, resp.response @@ -785,6 +785,7 @@ async def test_send_fcm_expired( config_entry: MockConfigEntry, load_config: MagicMock, mock_wp: AsyncMock, + mock_save: MagicMock, ) -> None: """Test that the FCM target is removed when expired.""" load_config.return_value = {"device": SUBSCRIPTION_5} @@ -796,15 +797,13 @@ async def test_send_fcm_expired( assert config_entry.state is ConfigEntryState.LOADED mock_wp.send_async.return_value.status = 410 - with ( - patch("homeassistant.components.html5.notify.save_json") as mock_save, - ): - await hass.services.async_call( - "notify", - "html5", - {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}}, - blocking=True, - ) + + await hass.services.async_call( + "notify", + "html5", + {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}}, + blocking=True, + ) # "device" should be removed when expired. mock_save.assert_called_once_with(hass.config.path(html5.REGISTRATIONS_FILE), {}) @@ -817,6 +816,7 @@ async def test_send_fcm_expired_save_fails( load_config: MagicMock, caplog: pytest.LogCaptureFixture, mock_wp: AsyncMock, + mock_save: MagicMock, ) -> None: """Test that the FCM target remains after expiry if save_json fails.""" load_config.return_value = {"device": SUBSCRIPTION_5} @@ -828,18 +828,13 @@ async def test_send_fcm_expired_save_fails( assert config_entry.state is ConfigEntryState.LOADED mock_wp.send_async.return_value.status = 410 - with ( - patch( - "homeassistant.components.html5.notify.save_json", - side_effect=HomeAssistantError(), - ), - ): - await hass.services.async_call( - "notify", - "html5", - {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}}, - blocking=True, - ) + mock_save.side_effect = HomeAssistantError + await hass.services.async_call( + "notify", + "html5", + {"message": "Hello", "target": ["device"], "data": {"icon": "beer.png"}}, + blocking=True, + ) # "device" should still exist if save fails. assert "Error saving registration" in caplog.text @@ -974,6 +969,7 @@ async def test_send_message_save_fails( webpush_async: AsyncMock, load_config: MagicMock, caplog: pytest.LogCaptureFixture, + mock_save: MagicMock, ) -> None: """Test sending a message with channel expired but saving registration fails.""" load_config.return_value = {"my-desktop": SUBSCRIPTION_1} @@ -987,13 +983,8 @@ async def test_send_message_save_fails( webpush_async.side_effect = ( WebPushException("", response=Mock(status=HTTPStatus.GONE)), ) - with ( - patch( - "homeassistant.components.html5.notify.save_json", - side_effect=HomeAssistantError, - ), - pytest.raises(HomeAssistantError) as e, - ): + mock_save.side_effect = HomeAssistantError + with pytest.raises(HomeAssistantError) as e: await hass.services.async_call( NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, From 8a187190a15dd3585e9779dfc1b97a21338a1aa3 Mon Sep 17 00:00:00 2001 From: Alex Fishlock Date: Sun, 13 Sep 2026 10:41:33 +0100 Subject: [PATCH 02/19] Add a maximum volume sensor to Lyngdorf (#181800) --- homeassistant/components/lyngdorf/icons.json | 3 + .../components/lyngdorf/quality_scale.yaml | 6 +- homeassistant/components/lyngdorf/sensor.py | 34 +++++++++-- .../components/lyngdorf/strings.json | 3 + tests/components/lyngdorf/conftest.py | 14 ++++- .../lyngdorf/snapshots/test_sensor.ambr | 51 ++++++++++++++++ tests/components/lyngdorf/test_sensor.py | 60 ++++++++++++++++++- 7 files changed, 161 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lyngdorf/icons.json b/homeassistant/components/lyngdorf/icons.json index d03976f606f34a..70e49a75340679 100644 --- a/homeassistant/components/lyngdorf/icons.json +++ b/homeassistant/components/lyngdorf/icons.json @@ -35,6 +35,9 @@ "audio_input": { "default": "mdi:audio-input-stereo-minijack" }, + "maximum_volume": { + "default": "mdi:volume-high" + }, "streaming_source": { "default": "mdi:cast-audio" }, diff --git a/homeassistant/components/lyngdorf/quality_scale.yaml b/homeassistant/components/lyngdorf/quality_scale.yaml index 1b67de8479410d..bdd129f4ae4cf2 100644 --- a/homeassistant/components/lyngdorf/quality_scale.yaml +++ b/homeassistant/components/lyngdorf/quality_scale.yaml @@ -69,8 +69,10 @@ rules: entity-disabled-by-default: status: done comment: >- - All entities are useful by default; the diagnostic sensors change only on - source or content changes. + The maximum volume sensor is disabled by default: most owners set no + ceiling and it would read the same value forever. Everything else is + useful by default; the diagnostic sensors change only on source or + content changes. entity-translations: done exception-translations: done icon-translations: done diff --git a/homeassistant/components/lyngdorf/sensor.py b/homeassistant/components/lyngdorf/sensor.py index 6a7b4b27d0ad68..488e2aeb10c3ee 100644 --- a/homeassistant/components/lyngdorf/sensor.py +++ b/homeassistant/components/lyngdorf/sensor.py @@ -4,14 +4,14 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, override -from lyngdorf import LyngdorfReceiver +from lyngdorf import LyngdorfReceiver, VolumeControl from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import EntityCategory +from homeassistant.const import EntityCategory, UnitOfSoundPressure from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -26,7 +26,7 @@ class LyngdorfSensorEntityDescription(SensorEntityDescription): """Describe a Lyngdorf sensor entity.""" - value_fn: Callable[[LyngdorfReceiver], str | None] + value_fn: Callable[[LyngdorfReceiver], str | float | None] options_fn: Callable[[LyngdorfReceiver], list[str]] | None = None @@ -98,6 +98,22 @@ def _known(value: str | None, options: list[str]) -> str | None: ) +# Only the models that report `!MAXVOL` carry a VolumeControl, so the ceiling +# sensor is created from the control's type. Its value stays None until the +# device first reports one, which never means the model has no ceiling. +MAXIMUM_VOLUME_SENSOR = LyngdorfSensorEntityDescription( + key="maximum_volume", + translation_key="maximum_volume", + native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, + value_fn=lambda r: ( + volume.maximum_volume if isinstance(volume := r.volume, VolumeControl) else None + ), + entity_category=EntityCategory.DIAGNOSTIC, + # Most owners set no ceiling, so this would read the same value forever. + entity_registry_enabled_default=False, +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: LyngdorfConfigEntry, @@ -112,6 +128,16 @@ async def async_setup_entry( ) for description in MAIN_ZONE_SENSORS ] + if isinstance(runtime_data.receiver.volume, VolumeControl): + entities.append( + LyngdorfSensor( + runtime_data.receiver, + config_entry, + runtime_data.device_info, + MAXIMUM_VOLUME_SENSOR, + ) + ) + # Zone B sensors stay on the main device so they read "Zone B audio input" # rather than repeating the zone in the Zone B device's own name. if runtime_data.zone_b_device_info is not None: @@ -157,6 +183,6 @@ def options(self) -> list[str] | None: @override @property - def native_value(self) -> str | None: + def native_value(self) -> str | float | None: """Return the current sensor value.""" return self.entity_description.value_fn(self._receiver) diff --git a/homeassistant/components/lyngdorf/strings.json b/homeassistant/components/lyngdorf/strings.json index 7536d548711313..9f145ddbe1739c 100644 --- a/homeassistant/components/lyngdorf/strings.json +++ b/homeassistant/components/lyngdorf/strings.json @@ -89,6 +89,9 @@ "audio_input": { "name": "Audio input" }, + "maximum_volume": { + "name": "Maximum volume" + }, "streaming_source": { "name": "Streaming source" }, diff --git a/tests/components/lyngdorf/conftest.py b/tests/components/lyngdorf/conftest.py index 0f8311a45aa8c9..4fb83fce8c4a51 100644 --- a/tests/components/lyngdorf/conftest.py +++ b/tests/components/lyngdorf/conftest.py @@ -15,6 +15,7 @@ RemoteKey, SteppableControl, Trim, + VolumeControl, ZoneB, ) import pytest @@ -71,6 +72,15 @@ def _steppable(value: float | None, value_range: NumericRange) -> MagicMock: return control +def _volume_control(value: float | None, value_range: NumericRange) -> MagicMock: + """Return a mocked volume control, as the MP and P models report.""" + control = MagicMock(spec=VolumeControl) + control.value = value + control.range = value_range + control.maximum_volume = None + return control + + def _control(value: float | None, value_range: NumericRange) -> MagicMock: """Return a mocked numeric control.""" control = MagicMock(spec=NumericControl) @@ -130,7 +140,7 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.zone_b_volume_range = NumericRange(-99.9, 24.0, 0.1) receiver.power_on = False - receiver.volume = _steppable(-40.0, NumericRange(-99.9, 24.0, 0.1)) + receiver.volume = _volume_control(-40.0, NumericRange(-99.9, 24.0, 0.1)) receiver.muted = False receiver.sources = [] receiver.sound_modes = [] @@ -191,7 +201,7 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.zone_b = zone_b receiver.zone_b_streaming_source = "DLNA" - receiver.volume = _steppable(-40.0, NumericRange(-99.9, 24.0, 0.1)) + receiver.volume = _volume_control(-40.0, NumericRange(-99.9, 24.0, 0.1)) receiver.muted = False receiver.sources = [] receiver.sound_modes = [] diff --git a/tests/components/lyngdorf/snapshots/test_sensor.ambr b/tests/components/lyngdorf/snapshots/test_sensor.ambr index dd417716be64c1..d2dd76ad449e80 100644 --- a/tests/components/lyngdorf/snapshots/test_sensor.ambr +++ b/tests/components/lyngdorf/snapshots/test_sensor.ambr @@ -109,6 +109,57 @@ 'state': 'optical', }) # --- +# name: test_entities[sensor.mock_lyngdorf_maximum_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_lyngdorf_maximum_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Maximum volume', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Maximum volume', + 'platform': 'lyngdorf', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'maximum_volume', + 'unique_id': '0050c27c76b2_maximum_volume', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.mock_lyngdorf_maximum_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Mock Lyngdorf Maximum volume', + : , + }), + 'context': , + 'entity_id': 'sensor.mock_lyngdorf_maximum_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_entities[sensor.mock_lyngdorf_streaming_source-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/lyngdorf/test_sensor.py b/tests/components/lyngdorf/test_sensor.py index 23efd081437498..0937dbf961bc81 100644 --- a/tests/components/lyngdorf/test_sensor.py +++ b/tests/components/lyngdorf/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from lyngdorf import NumericRange import pytest from syrupy.assertion import SnapshotAssertion @@ -9,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import notify_receiver_update +from .conftest import _steppable, notify_receiver_update from tests.common import MockConfigEntry, snapshot_platform @@ -20,7 +21,7 @@ def platforms() -> list[Platform]: return [Platform.SENSOR] -@pytest.mark.usefixtures("mock_receiver") +@pytest.mark.usefixtures("mock_receiver", "entity_registry_enabled_by_default") async def test_entities( hass: HomeAssistant, init_integration: MockConfigEntry, @@ -131,3 +132,58 @@ async def test_enum_options_follow_the_device( state = hass.states.get("sensor.mock_lyngdorf_audio_input") assert state.attributes["options"] == ["HDMI", "optical", "ARC"] + + +async def test_maximum_volume_is_disabled_by_default( + hass: HomeAssistant, + init_integration: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test the ceiling sensor exists but is off by default.""" + entry = entity_registry.async_get("sensor.mock_lyngdorf_maximum_volume") + + assert entry is not None + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert hass.states.get("sensor.mock_lyngdorf_maximum_volume") is None + + +async def test_maximum_volume_follows_the_device( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_receiver: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test the ceiling sensor reports what the device says, once enabled.""" + entity_id = "sensor.mock_lyngdorf_maximum_volume" + mock_receiver.volume.maximum_volume = 0.0 + + entity_registry.async_update_entity(entity_id, disabled_by=None) + await hass.config_entries.async_reload(init_integration.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "0.0" + + # The ceiling changes from the device's front panel, so it must not be + # read once and cached. + mock_receiver.volume.maximum_volume = -20.0 + notify_receiver_update(mock_receiver) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "-20.0" + + +async def test_no_maximum_volume_sensor_without_a_volume_control( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_receiver: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test a model that reports no ceiling does not get the sensor.""" + mock_config_entry.add_to_hass(hass) + mock_receiver.volume = _steppable(-40.0, NumericRange(-99.9, 24.0, 0.1)) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get("sensor.mock_lyngdorf_maximum_volume") is None + assert hass.states.get("sensor.mock_lyngdorf_audio_information") is not None From 6eff43cd3c08ff7371a0e64e75e1668fb4bc49f3 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sun, 13 Sep 2026 12:42:36 +0300 Subject: [PATCH 03/19] Add device_id support for MCP Server (#182057) --- homeassistant/components/mcp_server/server.py | 11 +- tests/components/mcp_server/test_http.py | 200 +++++++++++++++++- 2 files changed, 208 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mcp_server/server.py b/homeassistant/components/mcp_server/server.py index 52059d93ac9e6b..80d5169b1184b8 100644 --- a/homeassistant/components/mcp_server/server.py +++ b/homeassistant/components/mcp_server/server.py @@ -8,6 +8,7 @@ """ from collections.abc import Callable, Sequence +from dataclasses import replace import json import logging from typing import Any, cast @@ -31,6 +32,7 @@ SNAPSHOT_RESOURCE_URL = AnyUrl(SNAPSHOT_RESOURCE_URI) SNAPSHOT_RESOURCE_MIME_TYPE = "text/plain" LIVE_CONTEXT_TOOL_NAME = "homeassistant__GetLiveContext" +META_DEVICE_ID = "io.home-assistant/device_id" def _has_live_context_tool(llm_api: llm.APIInstance) -> bool: @@ -68,8 +70,15 @@ async def create_server( async def get_api_instance() -> llm.APIInstance: """Get the LLM API selected.""" + meta = server.request_context.meta + device_id = getattr(meta, META_DEVICE_ID, None) + if device_id is not None and not isinstance(device_id, str): + raise ValueError(f"{META_DEVICE_ID} must be a string") + # Backwards compatibility with old MCP Server config - return await llm.async_get_api(hass, llm_api_id, llm_context) + return await llm.async_get_api( + hass, llm_api_id, replace(llm_context, device_id=device_id) + ) @server.list_prompts() # type: ignore[no-untyped-call,untyped-decorator] async def handle_list_prompts() -> list[types.Prompt]: diff --git a/tests/components/mcp_server/test_http.py b/tests/components/mcp_server/test_http.py index a06332ab0a50fd..9ba896c7971f97 100644 --- a/tests/components/mcp_server/test_http.py +++ b/tests/components/mcp_server/test_http.py @@ -1,7 +1,7 @@ """Test the Model Context Protocol Server init module.""" -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager +from collections.abc import AsyncGenerator, Callable +from contextlib import AbstractAsyncContextManager, asynccontextmanager from http import HTTPStatus import json import logging @@ -18,6 +18,7 @@ from homeassistant.components.conversation import DOMAIN as CONVERSATION_DOMAIN from homeassistant.components.homeassistant.exposed_entities import async_expose_entity +from homeassistant.components.intent import async_register_timer_handler from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.mcp_server.const import DOMAIN, STATELESS_LLM_API from homeassistant.components.mcp_server.http import ( @@ -51,7 +52,12 @@ _LOGGER = logging.getLogger(__name__) TEST_ENTITY = "light.kitchen" +DEVICE_ID_META_KEY = "io.home-assistant/device_id" SNAPSHOT_RESOURCE_URI = "homeassistant://assist/context-snapshot" +type MCPClientFactory = Callable[ + [HomeAssistant, str, str], + AbstractAsyncContextManager[mcp.client.session.ClientSession], +] INITIALIZE_MESSAGE = { "jsonrpc": "2.0", "id": "request-id-1", @@ -390,6 +396,196 @@ def mcp_client_fixture(mcp_protocol: str) -> Any: raise ValueError(f"Unknown MCP protocol: {mcp_protocol}") +@pytest.mark.parametrize( + ("mcp_request", "result_type"), + [ + pytest.param( + mcp.types.ClientRequest(mcp.types.ListToolsRequest()), + mcp.types.ListToolsResult, + id="tools-list", + ), + pytest.param( + mcp.types.ClientRequest( + mcp.types.CallToolRequest( + params=mcp.types.CallToolRequestParams( + name="intent__HassTurnOn", + arguments={"name": "kitchen light"}, + ) + ) + ), + mcp.types.CallToolResult, + id="tools-call", + ), + pytest.param( + mcp.types.ClientRequest(mcp.types.ListPromptsRequest()), + mcp.types.ListPromptsResult, + id="prompts-list", + ), + pytest.param( + mcp.types.ClientRequest( + mcp.types.GetPromptRequest( + params=mcp.types.GetPromptRequestParams(name="Assist") + ) + ), + mcp.types.GetPromptResult, + id="prompts-get", + ), + pytest.param( + mcp.types.ClientRequest(mcp.types.ListResourcesRequest()), + mcp.types.ListResourcesResult, + id="resources-list", + ), + pytest.param( + mcp.types.ClientRequest.model_validate( + {"method": "resources/read", "params": {"uri": SNAPSHOT_RESOURCE_URI}} + ), + mcp.types.ReadResourceResult, + id="resources-read", + ), + ], +) +async def test_request_device_id( + hass: HomeAssistant, + mcp_url: str, + mcp_client: MCPClientFactory, + hass_supervisor_access_token: str, + mcp_request: mcp.types.ClientRequest, + result_type: type[mcp.types.Result], +) -> None: + """Apply the caller device to each request without retaining it in the session.""" + request_data = mcp_request.model_dump(by_alias=True, exclude_none=True) + request_data.setdefault("params", {})["_meta"] = {DEVICE_ID_META_KEY: "test-device"} + + with patch( + "homeassistant.helpers.llm.async_get_api", wraps=llm.async_get_api + ) as mock_get_api: + async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session: + await session.send_request( + mcp.types.ClientRequest.model_validate(request_data), result_type + ) + assert mock_get_api.await_count > 0 + device_contexts = [call.args[2] for call in mock_get_api.await_args_list] + assert {context.device_id for context in device_contexts} == {"test-device"} + mock_get_api.reset_mock() + + await session.send_request(mcp_request, result_type) + + assert mock_get_api.await_count > 0 + contexts = [call.args[2] for call in mock_get_api.await_args_list] + assert {context.device_id for context in contexts} == {None} + assert {context.device_id for context in device_contexts} == {"test-device"} + + +@pytest.mark.parametrize( + "metadata", + [ + pytest.param({}, id="empty"), + pytest.param({"other": "value"}, id="unrelated"), + pytest.param({DEVICE_ID_META_KEY: None}, id="null-device"), + ], +) +async def test_request_metadata_without_device_id( + hass: HomeAssistant, + mcp_url: str, + mcp_client: MCPClientFactory, + hass_supervisor_access_token: str, + metadata: dict[str, str | None], +) -> None: + """Metadata without a caller device keeps the default LLM context.""" + with patch( + "homeassistant.helpers.llm.async_get_api", wraps=llm.async_get_api + ) as mock_get_api: + async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session: + await session.list_tools( + params=mcp.types.PaginatedRequestParams( + _meta=mcp.types.RequestParams.Meta.model_validate(metadata) + ) + ) + + mock_get_api.assert_awaited_once() + assert mock_get_api.await_args.args[2].device_id is None + + +@pytest.mark.parametrize( + "device_id", + [ + pytest.param(123, id="number"), + pytest.param(True, id="boolean"), + pytest.param(["test-device"], id="list"), + pytest.param({"id": "test-device"}, id="object"), + ], +) +async def test_request_invalid_device_id( + hass: HomeAssistant, + mcp_url: str, + mcp_client: MCPClientFactory, + hass_supervisor_access_token: str, + device_id: int | bool | list[str] | dict[str, str], +) -> None: + """Reject caller device metadata with an invalid type.""" + async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session: + with pytest.raises( + McpError, match="io.home-assistant/device_id must be a string" + ): + await session.list_tools( + params=mcp.types.PaginatedRequestParams( + _meta=mcp.types.RequestParams.Meta.model_validate( + {DEVICE_ID_META_KEY: device_id} + ) + ) + ) + + +async def test_tool_call_invalid_device_id( + hass: HomeAssistant, + mcp_url: str, + mcp_client: MCPClientFactory, + hass_supervisor_access_token: str, +) -> None: + """Invalid caller metadata returns a tool error without performing the action.""" + async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session: + result = await session.call_tool( + name="intent__HassTurnOn", + arguments={"name": "kitchen light"}, + meta={DEVICE_ID_META_KEY: 123}, + ) + + assert result.isError + assert result.content == [ + mcp.types.TextContent( + type="text", text="io.home-assistant/device_id must be a string" + ) + ] + assert hass.states.get(TEST_ENTITY).state == STATE_OFF + + +async def test_request_device_id_enables_timer_tools( + hass: HomeAssistant, + mcp_url: str, + mcp_client: MCPClientFactory, + hass_supervisor_access_token: str, +) -> None: + """Offer timer tools only for requests from a device that supports timers.""" + + def handle_timer(*args: object) -> None: + pass + + async_register_timer_handler(hass, "test-device", handle_timer) + + async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session: + result = await session.list_tools( + params=mcp.types.PaginatedRequestParams( + _meta=mcp.types.RequestParams.Meta.model_validate( + {DEVICE_ID_META_KEY: "test-device"} + ) + ) + ) + assert "intent__HassStartTimer" in {tool.name for tool in result.tools} + + result = await session.list_tools() + assert "intent__HassStartTimer" not in {tool.name for tool in result.tools} + + @pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST, STATELESS_LLM_API]) async def test_mcp_tools_list( hass: HomeAssistant, From 9a0190eaf1a9cfb8297f6847453d11c7e0fca3db Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:46:52 +0200 Subject: [PATCH 04/19] Allow to configure the FRITZ!Smarthome integration with an URL (#179839) --- homeassistant/components/fritzbox/__init__.py | 53 ++++++- .../components/fritzbox/config_flow.py | 112 +++++++++----- homeassistant/components/fritzbox/const.py | 3 +- .../components/fritzbox/coordinator.py | 3 +- .../components/fritzbox/strings.json | 19 ++- tests/components/fritzbox/__init__.py | 4 +- tests/components/fritzbox/const.py | 11 +- tests/components/fritzbox/test_config_flow.py | 137 +++++++++++++----- tests/components/fritzbox/test_init.py | 120 ++++++++++++++- 9 files changed, 374 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 6974b27da3c7ac..3fe0fbd2cf19c4 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,14 +1,20 @@ """Support for AVM FRITZ!SmartHome devices.""" from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError +from yarl import URL from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, UnitOfTemperature +from homeassistant.const import ( + CONF_HOST, + CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, + UnitOfTemperature, +) from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.device_registry import AnyDeviceEntry from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from .const import DOMAIN, LOGGER, PLATFORMS +from .const import DEFAULT_VERIFY_SSL, DOMAIN, LOGGER, PLATFORMS from .coordinator import FritzboxConfigEntry, FritzboxDataUpdateCoordinator @@ -65,6 +71,49 @@ async def async_unload_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) -> return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) +async def async_migrate_entry( + hass: HomeAssistant, config_entry: FritzboxConfigEntry +) -> bool: + """Migrate old config entry to a new format.""" + LOGGER.debug( + "Migrating configuration from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + + if config_entry.version == 1: + new_data = {**config_entry.data} + if config_entry.minor_version < 2: + LOGGER.debug("Migrate config entry data to URL based configuration") + if "://" not in config_entry.data[CONF_HOST]: + host = URL().build( + scheme="http", + host=config_entry.data[CONF_HOST], + ) + else: + host = config_entry.data[CONF_HOST] + + new_data = { + **config_entry.data, + CONF_HOST: str(host), + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + } + + hass.config_entries.async_update_entry( + config_entry, data=new_data, version=1, minor_version=2 + ) + + LOGGER.debug( + "Migration to configuration version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + return True + + async def async_remove_config_entry_device( hass: HomeAssistant, entry: FritzboxConfigEntry, device: AnyDeviceEntry ) -> bool: diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 520fe5253dded6..25087d19bc415d 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -2,35 +2,54 @@ from collections.abc import Mapping import ipaddress -from typing import Any, Self, override -from urllib.parse import urlparse +from typing import TYPE_CHECKING, Any, Self, override from pyfritzhome import Fritzhome, LoginError from requests.exceptions import HTTPError import voluptuous as vol +from yarl import URL from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) from homeassistant.helpers.service_info.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_PRESENTATION_URL, ATTR_UPNP_UDN, SsdpServiceInfo, ) -from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN +from .const import DEFAULT_URL, DEFAULT_USERNAME, DEFAULT_VERIFY_SSL, DOMAIN DATA_SCHEMA_USER = vol.Schema( { - vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_URL, default=DEFAULT_URL): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.URL) + ), vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PASSWORD): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, } ) DATA_SCHEMA_CONFIRM = vol.Schema( { vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PASSWORD): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), } ) @@ -44,22 +63,25 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a AVM FRITZ!SmartHome config flow.""" VERSION = 1 + MINOR_VERSION = 2 _name: str def __init__(self) -> None: """Initialize flow.""" - self._host: str | None = None + self._url: str | None = None self._password: str | None = None self._username: str | None = None + self._verify_ssl: bool = DEFAULT_VERIFY_SSL def _get_entry(self, name: str) -> ConfigFlowResult: return self.async_create_entry( title=name, data={ - CONF_HOST: self._host, + CONF_HOST: self._url, CONF_PASSWORD: self._password, CONF_USERNAME: self._username, + CONF_VERIFY_SSL: self._verify_ssl, }, ) @@ -70,7 +92,10 @@ async def async_try_connect(self) -> str: def _try_connect(self) -> str: """Try to connect and check auth.""" fritzbox = Fritzhome( - host=self._host, user=self._username, password=self._password + host=self._url, + user=self._username, + password=self._password, + ssl_verify=self._verify_ssl, ) try: fritzbox.login() @@ -92,12 +117,13 @@ async def async_step_user( errors = {} if user_input is not None: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) - - self._host = user_input[CONF_HOST] - self._name = str(user_input[CONF_HOST]) + self._url = user_input[CONF_URL] + self._verify_ssl = user_input[CONF_VERIFY_SSL] self._password = user_input[CONF_PASSWORD] self._username = user_input[CONF_USERNAME] + self._name = str(self._url) + + self._async_abort_entries_match({CONF_HOST: self._url}) result = await self.async_try_connect() @@ -116,32 +142,40 @@ async def async_step_ssdp( self, discovery_info: SsdpServiceInfo ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" - host = urlparse(discovery_info.ssdp_location).hostname - assert isinstance(host, str) + if upnp_repr_udl := discovery_info.upnp.get(ATTR_UPNP_PRESENTATION_URL): + self._url = upnp_repr_udl + else: + assert isinstance(discovery_info.ssdp_location, str) + host = URL(discovery_info.ssdp_location).host + assert isinstance(host, str) + self._url = f"http://{host}" + representation_url = URL(self._url) + + if TYPE_CHECKING: + assert isinstance(representation_url.host, str) if ( - ipaddress.ip_address(host).version == 6 - and ipaddress.ip_address(host).is_link_local + ipaddress.ip_address(representation_url.host).version == 6 + and ipaddress.ip_address(representation_url.host).is_link_local ): return self.async_abort(reason="ignore_ip6_link_local") if uuid := discovery_info.upnp.get(ATTR_UPNP_UDN): uuid = uuid.removeprefix("uuid:") await self.async_set_unique_id(uuid) - self._abort_if_unique_id_configured({CONF_HOST: host}) + self._abort_if_unique_id_configured({CONF_HOST: self._url}) - self._host = host if self.hass.config_entries.flow.async_has_matching_flow(self): return self.async_abort(reason="already_in_progress") # update old and user-configured config entries for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == host: + if entry.data[CONF_HOST] == self._url: if uuid and not entry.unique_id: self.hass.config_entries.async_update_entry(entry, unique_id=uuid) return self.async_abort(reason="already_configured") - self._name = str(discovery_info.upnp.get(ATTR_UPNP_FRIENDLY_NAME) or host) + self._name = str(discovery_info.upnp.get(ATTR_UPNP_FRIENDLY_NAME) or self._url) self.context["title_placeholders"] = {"name": self._name} return await self.async_step_confirm() @@ -149,7 +183,7 @@ async def async_step_ssdp( @override def is_matching(self, other_flow: Self) -> bool: """Return True if other_flow is matching this flow.""" - return other_flow._host == self._host + return other_flow._url == self._url async def async_step_confirm( self, user_input: dict[str, Any] | None = None @@ -179,7 +213,8 @@ async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Trigger a reauthentication flow.""" - self._host = entry_data[CONF_HOST] + self._url = entry_data[CONF_HOST] + self._verify_ssl = entry_data[CONF_VERIFY_SSL] self._name = str(entry_data[CONF_HOST]) self._username = entry_data[CONF_USERNAME] @@ -200,8 +235,7 @@ async def async_step_reauth_confirm( if result == RESULT_SUCCESS: return self.async_update_reload_and_abort( self._get_reauth_entry(), - data={ - CONF_HOST: self._host, + data_updates={ CONF_PASSWORD: self._password, CONF_USERNAME: self._username, }, @@ -227,31 +261,39 @@ async def async_step_reconfigure( ) -> ConfigFlowResult: """Handle a reconfiguration flow initialized by the user.""" errors = {} + reconfigure_entry = self._get_reconfigure_entry() - if user_input is not None: - self._host = user_input[CONF_HOST] + self._url = reconfigure_entry.data[CONF_HOST] + self._verify_ssl = reconfigure_entry.data[CONF_VERIFY_SSL] + self._username = reconfigure_entry.data[CONF_USERNAME] + self._password = reconfigure_entry.data[CONF_PASSWORD] - reconfigure_entry = self._get_reconfigure_entry() - self._username = reconfigure_entry.data[CONF_USERNAME] - self._password = reconfigure_entry.data[CONF_PASSWORD] + if user_input is not None: + self._url = user_input[CONF_URL] + self._verify_ssl = user_input[CONF_VERIFY_SSL] result = await self.async_try_connect() if result == RESULT_SUCCESS: return self.async_update_reload_and_abort( reconfigure_entry, - data_updates={CONF_HOST: self._host}, + data_updates={ + CONF_HOST: self._url, + CONF_VERIFY_SSL: self._verify_ssl, + }, ) errors["base"] = result - host = self._get_reconfigure_entry().data[CONF_HOST] return self.async_show_form( step_id="reconfigure", data_schema=vol.Schema( { - vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_URL, default=self._url): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.URL) + ), + vol.Required(CONF_VERIFY_SSL, default=self._verify_ssl): bool, } ), - description_placeholders={"name": host}, + description_placeholders={"name": self._url}, errors=errors, ) diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 9f1de1e7dea5fe..0fa84965aa9fd4 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -13,8 +13,9 @@ COLOR_MODE: Final = "1" COLOR_TEMP_MODE: Final = "4" -DEFAULT_HOST: Final = "fritz.box" +DEFAULT_URL: Final = "http://fritz.box" DEFAULT_USERNAME: Final = "admin" +DEFAULT_VERIFY_SSL: Final = True DOMAIN: Final = "fritzbox" diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 1518ccfaa4df9e..3aae66e66dacca 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -9,7 +9,7 @@ from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -62,6 +62,7 @@ async def async_setup(self) -> None: host=self.config_entry.data[CONF_HOST], user=self.config_entry.data[CONF_USERNAME], password=self.config_entry.data[CONF_PASSWORD], + ssl_verify=self.config_entry.data[CONF_VERIFY_SSL], timeout=20, ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index 5c9adede2aee73..6db4981baf2b6c 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -1,7 +1,8 @@ { "common": { - "data_description_host": "The hostname or IP address of your FRITZ!Box router.", "data_description_password": "Password for the user to connect Home Assistant to your FRITZ!Box.", + "data_description_ssl_verify": "Whether to verify the SSL certificate when SSL encryption is used to connect to your FRITZ!Box.", + "data_description_url": "The URL of your FRITZ!Box.", "data_description_username": "Name of the user to connect Home Assistant to your FRITZ!Box." }, "config": { @@ -42,23 +43,27 @@ }, "reconfigure": { "data": { - "host": "[%key:common::config_flow::data::host%]" + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "host": "[%key:component::fritzbox::common::data_description_host%]" + "url": "[%key:component::fritzbox::common::data_description_url%]", + "verify_ssl": "[%key:component::fritzbox::common::data_description_ssl_verify%]" }, "description": "Update your configuration information for {name}." }, "user": { "data": { - "host": "[%key:common::config_flow::data::host%]", "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "url": "[%key:common::config_flow::data::url%]", + "username": "[%key:common::config_flow::data::username%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "host": "[%key:component::fritzbox::common::data_description_host%]", "password": "[%key:component::fritzbox::common::data_description_password%]", - "username": "[%key:component::fritzbox::common::data_description_username%]" + "url": "[%key:component::fritzbox::common::data_description_url%]", + "username": "[%key:component::fritzbox::common::data_description_username%]", + "verify_ssl": "[%key:component::fritzbox::common::data_description_ssl_verify%]" }, "description": "Enter your FRITZ!Box information." } diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index f7bfc44d51efaf..81400f538d7569 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -27,9 +27,7 @@ async def setup_config_entry( ) -> MockConfigEntry: """Do setup of a MockConfigEntry.""" entry = MockConfigEntry( - domain=DOMAIN, - data=data, - unique_id=unique_id, + domain=DOMAIN, data=data, unique_id=unique_id, version=1, minor_version=2 ) entry.add_to_hass(hass) if device is not None and fritz is not None: diff --git a/tests/components/fritzbox/const.py b/tests/components/fritzbox/const.py index c2895de6b8e15f..bdb606bb82efec 100644 --- a/tests/components/fritzbox/const.py +++ b/tests/components/fritzbox/const.py @@ -1,15 +1,22 @@ """Constants for fritzbox tests.""" from homeassistant.components.fritzbox.const import DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, +) MOCK_CONFIG = { DOMAIN: { CONF_DEVICES: [ { - CONF_HOST: "10.0.0.1", + CONF_HOST: "http://10.0.0.1", CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, } ] } diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 1f51b4310dafcf..04aa7c6ccefa04 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -3,7 +3,6 @@ import dataclasses from unittest import mock from unittest.mock import Mock, patch -from urllib.parse import urlparse from pyfritzhome import LoginError import pytest @@ -11,11 +10,19 @@ from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_PRESENTATION_URL, ATTR_UPNP_UDN, SsdpServiceInfo, ) @@ -24,33 +31,51 @@ from tests.common import MockConfigEntry -MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] +MOCK_USER_DATA = { + CONF_URL: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, +} MOCK_SSDP_DATA = { "ip4_valid": SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="https://10.0.0.1:12345/test", + ssdp_location="http://10.0.0.1:49000/fboxdesc.xml", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, + ATTR_UPNP_UDN: "uuid:only-a-test", + ATTR_UPNP_PRESENTATION_URL: "http://10.0.0.1", + }, + ), + "ip4_ssdp_fallback": SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://10.0.0.1:49000/fboxdesc.xml", upnp={ ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, ATTR_UPNP_UDN: "uuid:only-a-test", + ATTR_UPNP_PRESENTATION_URL: "", }, ), "ip6_valid": SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="https://[1234::1]:12345/test", + ssdp_location="http://[1234::1]:49000/fboxdesc.xml", upnp={ ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, ATTR_UPNP_UDN: "uuid:only-a-test", + ATTR_UPNP_PRESENTATION_URL: "http://[1234::1]", }, ), "ip6_invalid": SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="https://[fe80::1%1]:12345/test", + ssdp_location="http://[fe80::1%1]:49000/fboxdesc.xml", upnp={ ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, ATTR_UPNP_UDN: "uuid:only-a-test", + ATTR_UPNP_PRESENTATION_URL: "https://[fe80::1%1]", }, ), } @@ -78,10 +103,11 @@ async def test_user(hass: HomeAssistant, fritz: Mock) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "10.0.0.1" - assert result["data"][CONF_HOST] == "10.0.0.1" + assert result["title"] == "http://10.0.0.1" + assert result["data"][CONF_HOST] == "http://10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert result["data"][CONF_VERIFY_SSL] is False assert not result["result"].unique_id @@ -123,7 +149,15 @@ async def test_user_not_successful(hass: HomeAssistant, fritz: Mock) -> None: async def test_user_already_configured(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a flow by user when already configured.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, + }, + ) mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -141,7 +175,12 @@ async def test_user_already_configured(hass: HomeAssistant, fritz: Mock) -> None async def test_reauth_success(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reauthentication flow.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) result = await mock_config.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -165,7 +204,12 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reauthentication flow with authentication failure.""" fritz().login.side_effect = LoginError("Boom") - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) result = await mock_config.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -188,7 +232,12 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reauthentication flow but no connection found.""" fritz().login.side_effect = OSError("Boom") - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) result = await mock_config.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -208,10 +257,15 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: async def test_reconfigure_success(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reconfigure flow.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) - assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_HOST] == "http://10.0.0.1" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" @@ -221,26 +275,30 @@ async def test_reconfigure_success(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_HOST: "new_host", - }, + user_input={CONF_URL: "https://new_host:8443", CONF_VERIFY_SSL: True}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_HOST] == "https://new_host:8443" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" + assert mock_config.data[CONF_VERIFY_SSL] is True async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reconfigure flow with failure.""" fritz().login.side_effect = [OSError("Boom"), None] - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) - assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_HOST] == "http://10.0.0.1" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" @@ -250,9 +308,7 @@ async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_HOST: "new_host", - }, + user_input={CONF_URL: "https://new_host:8443", CONF_VERIFY_SSL: True}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" @@ -260,30 +316,31 @@ async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_HOST: "new_host", - }, + user_input={CONF_URL: "https://new_host:8443", CONF_VERIFY_SSL: True}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_HOST] == "https://new_host:8443" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" + assert mock_config.data[CONF_VERIFY_SSL] is True @pytest.mark.parametrize( - ("test_data", "expected_result"), + ("test_data", "expected_host", "expected_result"), [ - (MOCK_SSDP_DATA["ip4_valid"], FlowResultType.FORM), - (MOCK_SSDP_DATA["ip6_valid"], FlowResultType.FORM), - (MOCK_SSDP_DATA["ip6_invalid"], FlowResultType.ABORT), + (MOCK_SSDP_DATA["ip4_valid"], "http://10.0.0.1", FlowResultType.FORM), + (MOCK_SSDP_DATA["ip4_ssdp_fallback"], "http://10.0.0.1", FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_valid"], "http://[1234::1]", FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_invalid"], None, FlowResultType.ABORT), ], ) async def test_ssdp( hass: HomeAssistant, fritz: Mock, test_data: SsdpServiceInfo, + expected_host: str | None, expected_result: str, ) -> None: """Test starting a flow from discovery.""" @@ -303,9 +360,10 @@ async def test_ssdp( ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONF_FAKE_NAME - assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname + assert result["data"][CONF_HOST] == expected_host assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert result["data"][CONF_VERIFY_SSL] is True assert result["result"].unique_id == "only-a-test" @@ -325,10 +383,11 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock) -> None: user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "10.0.0.1" - assert result["data"][CONF_HOST] == "10.0.0.1" + assert result["title"] == "http://10.0.0.1" + assert result["data"][CONF_HOST] == "http://10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert result["data"][CONF_VERIFY_SSL] is True assert result["result"].unique_id == "only-a-test" @@ -425,7 +484,15 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock) - async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a flow from discovery when already configured.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, + }, + ) mock_config.add_to_hass(hass) assert not mock_config.unique_id diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 25b64f4ac17091..3f2eb2ecf5de14 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -16,6 +16,7 @@ CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, UnitOfTemperature, @@ -37,12 +38,19 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: entries = hass.config_entries.async_entries() assert entries assert len(entries) == 1 - assert entries[0].data[CONF_HOST] == "10.0.0.1" + assert entries[0].data[CONF_HOST] == "http://10.0.0.1" assert entries[0].data[CONF_PASSWORD] == "fake_pass" assert entries[0].data[CONF_USERNAME] == "fake_user" + assert entries[0].data[CONF_VERIFY_SSL] is False assert fritz.call_count == 1 assert fritz.call_args_list == [ - call(host="10.0.0.1", password="fake_pass", user="fake_user", timeout=20) + call( + host="http://10.0.0.1", + password="fake_pass", + user="fake_user", + ssl_verify=False, + timeout=20, + ) ] @@ -84,6 +92,8 @@ async def test_update_unique_id( domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -143,6 +153,8 @@ async def test_update_unique_id_no_change( domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -168,6 +180,8 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock) -> None: domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id=entity_id, + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -207,6 +221,8 @@ async def test_logout_on_stop(hass: HomeAssistant, fritz: Mock) -> None: domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id=entity_id, + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -279,6 +295,8 @@ async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant) -> domain=DOMAIN, data={CONF_HOST: "any", **MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]}, unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) with patch( @@ -300,6 +318,8 @@ async def test_raise_config_entry_error_when_login_fail(hass: HomeAssistant) -> domain=DOMAIN, data={CONF_HOST: "any", **MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]}, unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) with patch( @@ -313,3 +333,99 @@ async def test_raise_config_entry_error_when_login_fail(hass: HomeAssistant) -> entries = hass.config_entries.async_entries() config_entry = entries[0] assert config_entry.state is ConfigEntryState.SETUP_ERROR + + +@pytest.mark.parametrize( + ("old_data", "new_data"), + [ + ( + { + CONF_HOST: "10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "https://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "https://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "1234::1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "http://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "http://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "http://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "https://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "https://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ], +) +async def test_migrate_entry( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + fritz: Mock, + old_data: dict, + new_data: dict, +) -> None: + """Test migrate config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=old_data, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.fritzbox.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.data == new_data From e333e978d4eac7d83e49b53c236b609aa1b0cddd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 13 Sep 2026 14:03:48 +0200 Subject: [PATCH 05/19] Bump nextdns to 5.1.0 (#182099) --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- tests/components/nextdns/fixtures/settings.json | 7 ++++++- tests/components/nextdns/snapshots/test_diagnostics.ambr | 5 +++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 850feb1c572938..386b0429aa7070 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["nextdns"], "quality_scale": "platinum", - "requirements": ["nextdns==5.0.1"] + "requirements": ["nextdns==5.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 922c60f37ff7ed..44e03db76fb570 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1728,7 +1728,7 @@ nextcloudmonitor==1.5.1 nextcord==3.1.0 # homeassistant.components.nextdns -nextdns==5.0.1 +nextdns==5.1.0 # homeassistant.components.niko_home_control nhc==0.8.1 diff --git a/tests/components/nextdns/fixtures/settings.json b/tests/components/nextdns/fixtures/settings.json index a7591cfcc7653b..3bebb1a0b8facf 100644 --- a/tests/components/nextdns/fixtures/settings.json +++ b/tests/components/nextdns/fixtures/settings.json @@ -74,5 +74,10 @@ "block_piracy": true, "block_porn": true, "block_social_networks": true, - "block_video_streaming": true + "block_video_streaming": true, + "block_data_drop_services": true, + "block_decentralized_web_gateways": true, + "block_free_hosting_domains": true, + "block_residential_hosting": true, + "block_tunneling_endpoints": true } diff --git a/tests/components/nextdns/snapshots/test_diagnostics.ambr b/tests/components/nextdns/snapshots/test_diagnostics.ambr index e585ad7696d385..0a7276b75dbd9b 100644 --- a/tests/components/nextdns/snapshots/test_diagnostics.ambr +++ b/tests/components/nextdns/snapshots/test_diagnostics.ambr @@ -73,14 +73,17 @@ 'block_chatgpt': True, 'block_csam': True, 'block_dailymotion': True, + 'block_data_drop_services': True, 'block_dating': True, 'block_ddns': True, + 'block_decentralized_web_gateways': True, 'block_discord': True, 'block_disguised_trackers': True, 'block_disneyplus': True, 'block_ebay': True, 'block_facebook': True, 'block_fortnite': True, + 'block_free_hosting_domains': True, 'block_gambling': True, 'block_google_chat': True, 'block_hbomax': True, @@ -102,6 +105,7 @@ 'block_porn': True, 'block_primevideo': True, 'block_reddit': True, + 'block_residential_hosting': True, 'block_roblox': True, 'block_signal': True, 'block_skype': True, @@ -113,6 +117,7 @@ 'block_tiktok': True, 'block_tinder': True, 'block_tumblr': True, + 'block_tunneling_endpoints': True, 'block_twitch': True, 'block_twitter': True, 'block_video_streaming': True, From 4abc131cf92b4e7056ce62b2da35bbdb2bbd6711 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 13 Sep 2026 14:07:50 +0200 Subject: [PATCH 06/19] Add price type and interval options to EnergyZero energy price action (#181683) --- .../components/energyzero/services.py | 20 +- .../components/energyzero/services.yaml | 16 + .../components/energyzero/strings.json | 20 ++ tests/components/energyzero/test_services.py | 308 +++++++++++++++++- 4 files changed, 357 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/energyzero/services.py b/homeassistant/components/energyzero/services.py index bd191bbff908c5..ef1b90519e114e 100644 --- a/homeassistant/components/energyzero/services.py +++ b/homeassistant/components/energyzero/services.py @@ -27,6 +27,10 @@ ATTR_START: Final = "start" ATTR_END: Final = "end" ATTR_INCL_VAT: Final = "incl_vat" +ATTR_PRICE_TYPE: Final = "price_type" +ATTR_INTERVAL: Final = "interval" + +ENERGY_INTERVALS = {"hour": Interval.HOUR, "quarter": Interval.QUARTER} GAS_SERVICE_NAME: Final = "get_gas_prices" ENERGY_SERVICE_NAME: Final = "get_energy_prices" @@ -43,6 +47,13 @@ } ) +ENERGY_SERVICE_SCHEMA: Final = SERVICE_SCHEMA.extend( + { + vol.Optional(ATTR_PRICE_TYPE, default="market"): vol.In(("market", "all_in")), + vol.Optional(ATTR_INTERVAL, default="hour"): vol.In(ENERGY_INTERVALS), + } +) + class ServicePriceType(Enum): """Type of service.""" @@ -142,6 +153,11 @@ async def __get_prices( PriceType.MARKET_WITH_VAT if call.data[ATTR_INCL_VAT] else PriceType.MARKET ) + if price_type is ServicePriceType.ENERGY and call.data[ATTR_PRICE_TYPE] == "all_in": + selected_price_type = ( + PriceType.ALL_IN if call.data[ATTR_INCL_VAT] else PriceType.ALL_IN_EXCL_VAT + ) + price_data: list[EnergyPrices] = [] for day_offset in range((end_date - start_date).days + 1): request_date = start_date + timedelta(days=day_offset) @@ -156,7 +172,7 @@ async def __get_prices( prices = coordinator.energyzero.get_electricity_prices( start_date=request_date, end_date=request_date, - interval=Interval.HOUR, + interval=ENERGY_INTERVALS[call.data[ATTR_INTERVAL]], price_type=selected_price_type, local_tz=local_tz, ) @@ -190,6 +206,6 @@ def async_setup_services(hass: HomeAssistant) -> None: DOMAIN, ENERGY_SERVICE_NAME, partial(__get_prices, price_type=ServicePriceType.ENERGY), - schema=SERVICE_SCHEMA, + schema=ENERGY_SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) diff --git a/homeassistant/components/energyzero/services.yaml b/homeassistant/components/energyzero/services.yaml index dc8df9aa6d0c4c..0c57f0de8bf9d5 100644 --- a/homeassistant/components/energyzero/services.yaml +++ b/homeassistant/components/energyzero/services.yaml @@ -32,6 +32,22 @@ get_energy_prices: default: true selector: boolean: + price_type: + default: market + selector: + select: + translation_key: price_type + options: + - market + - all_in + interval: + default: hour + selector: + select: + translation_key: interval + options: + - hour + - quarter start: required: false example: "2023-01-01 00:00:00" diff --git a/homeassistant/components/energyzero/strings.json b/homeassistant/components/energyzero/strings.json index 14047ff857806f..47b7bf23735b8a 100644 --- a/homeassistant/components/energyzero/strings.json +++ b/homeassistant/components/energyzero/strings.json @@ -87,6 +87,18 @@ "hourly": "Hourly", "quarter_hourly": "Quarter-hourly" } + }, + "interval": { + "options": { + "hour": "[%key:component::energyzero::selector::electricity_price_interval::options::hourly%]", + "quarter": "[%key:component::energyzero::selector::electricity_price_interval::options::quarter_hourly%]" + } + }, + "price_type": { + "options": { + "all_in": "All-in", + "market": "Market" + } } }, "services": { @@ -105,6 +117,14 @@ "description": "[%key:component::energyzero::services::get_gas_prices::fields::incl_vat::description%]", "name": "[%key:component::energyzero::services::get_gas_prices::fields::incl_vat::name%]" }, + "interval": { + "description": "The interval of electricity prices to retrieve. Defaults to hourly, independently of the electricity price interval configured for entities.", + "name": "Interval" + }, + "price_type": { + "description": "The type of electricity prices to retrieve. Defaults to market prices.", + "name": "Price type" + }, "start": { "description": "[%key:component::energyzero::services::get_gas_prices::fields::start::description%]", "name": "[%key:component::energyzero::services::get_gas_prices::fields::start::name%]" diff --git a/tests/components/energyzero/test_services.py b/tests/components/energyzero/test_services.py index bf99e96d9d3a59..04656d011bc24a 100644 --- a/tests/components/energyzero/test_services.py +++ b/tests/components/energyzero/test_services.py @@ -1,16 +1,25 @@ """Tests for the services provided by the EnergyZero integration.""" -from datetime import UTC, date, datetime +from datetime import UTC, date, datetime, timedelta import re -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, call from zoneinfo import ZoneInfo -from energyzero import EnergyPrices, EnergyZeroNoDataError, PriceType, TimeRange +from energyzero import ( + EnergyPrices, + EnergyZeroNoDataError, + Interval, + PriceType, + TimeRange, +) import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol -from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.components.energyzero.const import ( + CONF_ELECTRICITY_PRICE_INTERVAL, + DOMAIN, +) from homeassistant.components.energyzero.services import ( ATTR_CONFIG_ENTRY, ENERGY_SERVICE_NAME, @@ -514,12 +523,20 @@ async def test_service_called_with_unloaded_entry( @pytest.mark.usefixtures("init_integration") -@pytest.mark.parametrize("service", [GAS_SERVICE_NAME, ENERGY_SERVICE_NAME]) +@pytest.mark.parametrize( + ("service", "service_data"), + [ + (GAS_SERVICE_NAME, {}), + (ENERGY_SERVICE_NAME, {}), + (ENERGY_SERVICE_NAME, {"price_type": "all_in", "interval": "quarter"}), + ], +) async def test_service_no_data_returns_validation_error( hass: HomeAssistant, mock_energyzero: AsyncMock, mock_config_entry: MockConfigEntry, service: str, + service_data: dict[str, str], ) -> None: """Test backend no-data errors are surfaced as service validation errors.""" method = ( @@ -541,7 +558,288 @@ async def test_service_no_data_returns_validation_error( { ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, "incl_vat": True, + **service_data, + }, + blocking=True, + return_response=True, + ) + + +@pytest.mark.parametrize("entity_interval", ["hourly", "quarter_hourly"]) +@pytest.mark.parametrize( + ("interval_data", "expected_interval"), + [ + pytest.param({}, Interval.HOUR, id="default-hour"), + pytest.param({"interval": "hour"}, Interval.HOUR, id="hour"), + pytest.param({"interval": "quarter"}, Interval.QUARTER, id="quarter"), + ], +) +@pytest.mark.parametrize( + ("price_data", "incl_vat", "expected_price_type"), + [ + pytest.param({}, True, PriceType.MARKET_WITH_VAT, id="default-vat"), + pytest.param({}, False, PriceType.MARKET, id="default-no-vat"), + pytest.param( + {"price_type": "market"}, True, PriceType.MARKET_WITH_VAT, id="market-vat" + ), + pytest.param( + {"price_type": "market"}, False, PriceType.MARKET, id="market-no-vat" + ), + pytest.param({"price_type": "all_in"}, True, PriceType.ALL_IN, id="all-in-vat"), + pytest.param( + {"price_type": "all_in"}, + False, + PriceType.ALL_IN_EXCL_VAT, + id="all-in-no-vat", + ), + ], +) +async def test_energy_service_options( + hass: HomeAssistant, + mock_energyzero: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_interval: str, + interval_data: dict[str, str], + expected_interval: Interval, + price_data: dict[str, str], + incl_vat: bool, + expected_price_type: PriceType, +) -> None: + """Action options and defaults are independent of entity configuration.""" + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, options={CONF_ELECTRICITY_PRICE_INTERVAL: entity_interval} + ) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + coordinator = mock_config_entry.runtime_data + coordinator_data = coordinator.data + entity_states = hass.states.async_all() + mock_energyzero.reset_mock() + + await hass.services.async_call( + DOMAIN, + ENERGY_SERVICE_NAME, + { + ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, + "incl_vat": incl_vat, + **price_data, + **interval_data, + }, + blocking=True, + return_response=True, + ) + + mock_energyzero.get_electricity_prices.assert_awaited_once_with( + start_date=date(2026, 4, 10), + end_date=date(2026, 4, 10), + interval=expected_interval, + price_type=expected_price_type, + local_tz=ZoneInfo(hass.config.time_zone), + ) + mock_energyzero.get_gas_prices.assert_not_awaited() + assert coordinator.data is coordinator_data + assert hass.states.async_all() == entity_states + assert mock_config_entry.options == { + CONF_ELECTRICITY_PRICE_INTERVAL: entity_interval + } + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + ("start", "end", "first_timestamp", "period_count"), + [ + pytest.param( + "2026-04-10", "2026-04-10", "2026-04-09T22:00:00+00:00", 96, id="date-only" + ), + pytest.param( + "2026-04-10 00:07:00", + "2026-04-10 00:38:00", + "2026-04-09T22:00:00+00:00", + 3, + id="partial-periods", + ), + pytest.param( + "2026-04-10 00:15:00", + "2026-04-10 00:15:00", + "2026-04-09T22:00:00+00:00", + 96, + id="equal-times-full-day", + ), + pytest.param( + "2026-04-10 00:15:00", + "2026-04-10 00:30:00", + "2026-04-09T22:15:00+00:00", + 1, + id="exact-boundaries", + ), + pytest.param( + "2026-04-10 23:53:00+02:00", + "2026-04-11 00:07:00+02:00", + "2026-04-10T21:45:00+00:00", + 2, + id="multiple-days", + ), + pytest.param( + "2026-03-29", "2026-03-29", "2026-03-28T23:00:00+00:00", 92, id="spring-dst" + ), + pytest.param( + "2026-10-25", + "2026-10-25", + "2026-10-24T22:00:00+00:00", + 100, + id="autumn-dst", + ), + pytest.param( + "2026-03-29 01:53:00+01:00", + "2026-03-29 03:07:00+02:00", + "2026-03-29T00:45:00+00:00", + 2, + id="spring-overlap", + ), + pytest.param( + "2026-10-25 02:53:00+02:00", + "2026-10-25 02:07:00+01:00", + "2026-10-25T00:45:00+00:00", + 2, + id="autumn-overlap", + ), + ], +) +async def test_energy_service_quarter_ranges( + hass: HomeAssistant, + mock_energyzero: AsyncMock, + mock_config_entry: MockConfigEntry, + start: str, + end: str, + first_timestamp: str, + period_count: int, +) -> None: + """Filter actual quarter-hour ranges, including partial periods and DST.""" + await hass.config.async_set_time_zone("Europe/Amsterdam") + local_tz = ZoneInfo(hass.config.time_zone) + first_day = date.fromisoformat(start[:10]) + last_day = date.fromisoformat(end[:10]) + days = [ + first_day + timedelta(days=index) + for index in range((last_day - first_day).days + 1) + ] + step = timedelta(minutes=15) + datasets = [] + for day in days: + day_start = datetime.combine(day, datetime.min.time(), local_tz).astimezone(UTC) + day_end = datetime.combine( + day + timedelta(days=1), datetime.min.time(), local_tz + ).astimezone(UTC) + datasets.append( + EnergyPrices( + prices={ + TimeRange( + day_start + index * step, day_start + (index + 1) * step + ): 0.25 + for index in range((day_end - day_start) // step) + }, + average_price=0.25, + ) + ) + mock_energyzero.reset_mock() + mock_energyzero.get_electricity_prices.side_effect = datasets + + response = await hass.services.async_call( + DOMAIN, + ENERGY_SERVICE_NAME, + { + ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, + "incl_vat": True, + "price_type": "all_in", + "interval": "quarter", + "start": start, + "end": end, + }, + blocking=True, + return_response=True, + ) + + first = datetime.fromisoformat(first_timestamp) + assert response == { + "prices": [ + { + "price": 0.25, + "timestamp": str(first + index * step), + "start": str(first + index * step), + "end": str(first + (index + 1) * step), + } + for index in range(period_count) + ] + } + assert mock_energyzero.get_electricity_prices.await_args_list == [ + call( + start_date=day, + end_date=day, + interval=Interval.QUARTER, + price_type=PriceType.ALL_IN, + local_tz=local_tz, + ) + for day in days + ] + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + ("service", "service_data"), + [ + (ENERGY_SERVICE_NAME, {"price_type": "market_with_vat"}), + (ENERGY_SERVICE_NAME, {"interval": "day"}), + (GAS_SERVICE_NAME, {"price_type": "all_in"}), + (GAS_SERVICE_NAME, {"interval": "quarter"}), + ], +) +async def test_service_rejects_unsupported_options( + hass: HomeAssistant, + mock_energyzero: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + service_data: dict[str, str], +) -> None: + """Only the electricity action accepts the supported new field values.""" + mock_energyzero.reset_mock() + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + service, + { + ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, + "incl_vat": True, + **service_data, + }, + blocking=True, + return_response=True, + ) + mock_energyzero.get_electricity_prices.assert_not_awaited() + mock_energyzero.get_gas_prices.assert_not_awaited() + + +@pytest.mark.usefixtures("init_integration") +async def test_energy_service_quarter_invalid_range( + hass: HomeAssistant, + mock_energyzero: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Reject reversed quarter-hour ranges before calling the API.""" + mock_energyzero.reset_mock() + with pytest.raises(ServiceValidationError, match="Invalid date range provided"): + await hass.services.async_call( + DOMAIN, + ENERGY_SERVICE_NAME, + { + ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, + "incl_vat": True, + "price_type": "all_in", + "interval": "quarter", + "start": "2026-04-10 00:15:00", + "end": "2026-04-10 00:10:00", }, blocking=True, return_response=True, ) + mock_energyzero.get_electricity_prices.assert_not_awaited() From 80a2ea2d8dd10dd2b9024d7888a6ecaadf8155c3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:09:36 +0200 Subject: [PATCH 07/19] Bump aiolibrenms to 0.0.4 (#182097) --- homeassistant/components/librenms/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/librenms/manifest.json b/homeassistant/components/librenms/manifest.json index d8f8b7a7b1dc0f..5e5ece72e8a457 100644 --- a/homeassistant/components/librenms/manifest.json +++ b/homeassistant/components/librenms/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_polling", "loggers": ["aiolibrenms"], "quality_scale": "bronze", - "requirements": ["aiolibrenms==0.0.3"] + "requirements": ["aiolibrenms==0.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 44e03db76fb570..d976ee0f9f45d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -324,7 +324,7 @@ aiokef==0.2.16 aiokem==1.0.1 # homeassistant.components.librenms -aiolibrenms==0.0.3 +aiolibrenms==0.0.4 # homeassistant.components.lichess aiolichess==1.3.0 From 0ab651a7b17e2c168e69b95d4062b92efd4ca579 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Sep 2026 14:09:47 +0200 Subject: [PATCH 08/19] Bump probatio to 0.12.1 (#182101) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/mqtt/test_alarm_control_panel.py | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b99b34faf445e..bd1450ce010a09 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ orjson==3.11.9 packaging>=23.1 paho-mqtt==2.1.0 Pillow==12.3.0 -probatio==0.12.0 +probatio==0.12.1 propcache==0.5.2 psutil-home-assistant==0.0.1 PyJWT==2.13.0 diff --git a/pyproject.toml b/pyproject.toml index f0883e72a10b94..a24618011d919d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ dependencies = [ "ulid-transform==2.2.9", "urllib3>=2.0", "uv==0.12.10", - "probatio==0.12.0", + "probatio==0.12.1", "yarl==1.24.5", "webrtc-models==0.3.0", "zeroconf==0.151.3", diff --git a/requirements.txt b/requirements.txt index 20190a9b3f70cb..6b5c9b16d2c703 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ mutagen==1.48.1 orjson==3.11.9 packaging>=23.1 Pillow==12.3.0 -probatio==0.12.0 +probatio==0.12.1 propcache==0.5.2 psutil-home-assistant==0.0.1 PyJWT==2.13.0 diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index c43743069630c6..e0ab9d0d967206 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1379,8 +1379,7 @@ async def test_reload_after_invalid_config( assert await mqtt_mock_entry() assert hass.states.get("alarm_control_panel.test") is None assert ( - "not a valid option, did you mean 'availability_topic' or " - "'command_topic'? at 'invalid_topic' for " + "not a valid option at 'invalid_topic' for " "manually configured MQTT alarm_control_panel item, " "in ?, line ? Got {'name': 'test', 'invalid_topic': 'test-topic'}" in caplog.text From 862adee33c8b870d2bc561f677d89dc2fc9a5d23 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sun, 13 Sep 2026 15:12:17 +0300 Subject: [PATCH 09/19] Remove deprecated openai_conversation services (#181750) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../openai_conversation/__init__.py | 240 +------- .../components/openai_conversation/const.py | 1 - .../components/openai_conversation/icons.json | 10 - .../openai_conversation/quality_scale.yaml | 10 +- .../openai_conversation/services.yaml | 60 -- .../openai_conversation/strings.json | 60 -- .../openai_conversation/test_entity.py | 87 +++ .../openai_conversation/test_init.py | 546 +----------------- 8 files changed, 100 insertions(+), 914 deletions(-) delete mode 100644 homeassistant/components/openai_conversation/icons.json delete mode 100644 homeassistant/components/openai_conversation/services.yaml diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 67799727ed5455..5f6b19b641a1d7 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -1,53 +1,25 @@ """The OpenAI Conversation integration.""" -from pathlib import Path from types import MappingProxyType from typing import Any, cast import openai -from openai.types.images_response import ImagesResponse -from openai.types.responses import ( - EasyInputMessageParam, - Response, - ResponseInputMessageContentListParam, - ResponseInputParam, - ResponseInputTextParam, -) -import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigSubentry -from homeassistant.const import CONF_API_KEY, CONF_PROMPT, Platform -from homeassistant.core import ( - HomeAssistant, - ServiceCall, - ServiceResponse, - SupportsResponse, -) -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, - ServiceValidationError, -) +from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, - issue_registry as ir, - selector, ) from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from .const import ( CONF_CHAT_MODEL, - CONF_FILENAMES, - CONF_MAX_TOKENS, - CONF_REASONING_EFFORT, CONF_REASONING_SUMMARY, - CONF_STORE_RESPONSES, - CONF_TEMPERATURE, - CONF_TOP_P, DEFAULT_AI_TASK_NAME, DEFAULT_NAME, DEFAULT_STT_NAME, @@ -55,20 +27,10 @@ DOMAIN, LOGGER, RECOMMENDED_AI_TASK_OPTIONS, - RECOMMENDED_CHAT_MODEL, - RECOMMENDED_MAX_TOKENS, - RECOMMENDED_REASONING_EFFORT, RECOMMENDED_REASONING_SUMMARY, - RECOMMENDED_STORE_RESPONSES, RECOMMENDED_STT_OPTIONS, - RECOMMENDED_TEMPERATURE, - RECOMMENDED_TOP_P, RECOMMENDED_TTS_OPTIONS, ) -from .entity import async_prepare_files_for_prompt - -SERVICE_GENERATE_IMAGE = "generate_image" -SERVICE_GENERATE_CONTENT = "generate_content" PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION, Platform.STT, Platform.TTS) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) @@ -80,202 +42,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up OpenAI Conversation.""" await async_migrate_integration(hass) - async def render_image(call: ServiceCall) -> ServiceResponse: - """Render an image with dall-e.""" - LOGGER.warning( - "Action '%s.%s' is deprecated and will be removed in the 2026.9.0 release. " - "Please use the 'ai_task.generate_image' action instead", - DOMAIN, - SERVICE_GENERATE_IMAGE, - ) - ir.async_create_issue( - hass, - DOMAIN, - "deprecated_generate_image", - breaks_in_ha_version="2026.9.0", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_generate_image", - ) - - entry_id = call.data["config_entry"] - entry = hass.config_entries.async_get_entry(entry_id) - - if entry is None or entry.domain != DOMAIN: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="invalid_config_entry", - translation_placeholders={"config_entry": entry_id}, - ) - - client: openai.AsyncClient = entry.runtime_data - - try: - response: ImagesResponse = await client.images.generate( - model="dall-e-3", - prompt=call.data[CONF_PROMPT], - size=call.data["size"], - quality=call.data["quality"], - style=call.data["style"], - response_format="url", - n=1, - ) - except openai.AuthenticationError as err: - entry.async_start_reauth(hass) - raise HomeAssistantError("Authentication error") from err - except openai.OpenAIError as err: - raise HomeAssistantError(f"Error generating image: {err}") from err - - if not response.data or not response.data[0].url: - raise HomeAssistantError("No image returned") - - return response.data[0].model_dump(exclude={"b64_json"}) - - async def send_prompt(call: ServiceCall) -> ServiceResponse: - """Send a prompt to ChatGPT and return the response.""" - LOGGER.warning( - "Action '%s.%s' is deprecated and will be removed in the 2026.9.0 release. " - "Please use the 'ai_task.generate_data' action instead", - DOMAIN, - SERVICE_GENERATE_CONTENT, - ) - ir.async_create_issue( - hass, - DOMAIN, - "deprecated_generate_content", - breaks_in_ha_version="2026.9.0", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_generate_content", - ) - - entry_id = call.data["config_entry"] - entry = hass.config_entries.async_get_entry(entry_id) - - if entry is None or entry.domain != DOMAIN: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="invalid_config_entry", - translation_placeholders={"config_entry": entry_id}, - ) - - # Get first conversation subentry for options - conversation_subentry = next( - ( - sub - for sub in entry.subentries.values() - if sub.subentry_type == "conversation" - ), - None, - ) - if not conversation_subentry: - raise ServiceValidationError("No conversation configuration found") - - model: str = conversation_subentry.data.get( - CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL - ) - client: openai.AsyncClient = entry.runtime_data - - content: ResponseInputMessageContentListParam = [ - ResponseInputTextParam(type="input_text", text=call.data[CONF_PROMPT]) - ] - - if filenames := call.data.get(CONF_FILENAMES): - for filename in filenames: - if not hass.config.is_allowed_path(filename): - raise HomeAssistantError( - f"Cannot read `{filename}`, no access to path; " - "`allowlist_external_dirs` may need to be adjusted in " - "`configuration.yaml`" - ) - - content.extend( - await async_prepare_files_for_prompt( - hass, [(Path(filename), None) for filename in filenames] - ) - ) - - messages: ResponseInputParam = [ - EasyInputMessageParam(type="message", role="user", content=content) - ] - - model_args = { - "model": model, - "input": messages, - "max_output_tokens": conversation_subentry.data.get( - CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS - ), - "top_p": conversation_subentry.data.get(CONF_TOP_P, RECOMMENDED_TOP_P), - "temperature": conversation_subentry.data.get( - CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE - ), - "user": call.context.user_id, - "store": conversation_subentry.data.get( - CONF_STORE_RESPONSES, RECOMMENDED_STORE_RESPONSES - ), - } - - if model.startswith("o"): - model_args["reasoning"] = { - "effort": conversation_subentry.data.get( - CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT - ) - } - - try: - response: Response = await client.responses.create(**model_args) - except openai.AuthenticationError as err: - entry.async_start_reauth(hass) - raise HomeAssistantError("Authentication error") from err - except openai.OpenAIError as err: - raise HomeAssistantError(f"Error generating content: {err}") from err - except FileNotFoundError as err: - raise HomeAssistantError(f"Error generating content: {err}") from err - - return {"text": response.output_text} - - hass.services.async_register( - DOMAIN, - SERVICE_GENERATE_CONTENT, - send_prompt, - schema=vol.Schema( - { - vol.Required("config_entry"): selector.ConfigEntrySelector( - { - "integration": DOMAIN, - } - ), - vol.Required(CONF_PROMPT): cv.string, - vol.Optional(CONF_FILENAMES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), - supports_response=SupportsResponse.ONLY, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_GENERATE_IMAGE, - render_image, - schema=vol.Schema( - { - vol.Required("config_entry"): selector.ConfigEntrySelector( - { - "integration": DOMAIN, - } - ), - vol.Required(CONF_PROMPT): cv.string, - vol.Optional("size", default="1024x1024"): vol.In( - ("1024x1024", "1024x1792", "1792x1024") - ), - vol.Optional("quality", default="standard"): vol.In(("standard", "hd")), - vol.Optional("style", default="vivid"): vol.In(("vivid", "natural")), - } - ), - supports_response=SupportsResponse.ONLY, - ) - return True diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 78e7bf948582d0..518e8217a12ed0 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -18,7 +18,6 @@ CONF_CHAT_MODEL = "chat_model" CONF_IMAGE_MODEL = "image_model" CONF_CODE_INTERPRETER = "code_interpreter" -CONF_FILENAMES = "filenames" CONF_MAX_TOKENS = "max_tokens" CONF_PRO_MODE = "pro_mode" CONF_REASONING_EFFORT = "reasoning_effort" diff --git a/homeassistant/components/openai_conversation/icons.json b/homeassistant/components/openai_conversation/icons.json deleted file mode 100644 index 23cdc26ed713e2..00000000000000 --- a/homeassistant/components/openai_conversation/icons.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "services": { - "generate_content": { - "service": "mdi:receipt-text" - }, - "generate_image": { - "service": "mdi:image-sync" - } - } -} diff --git a/homeassistant/components/openai_conversation/quality_scale.yaml b/homeassistant/components/openai_conversation/quality_scale.yaml index 1a55ec2fbb41a1..99aa0cb06f3a87 100644 --- a/homeassistant/components/openai_conversation/quality_scale.yaml +++ b/homeassistant/components/openai_conversation/quality_scale.yaml @@ -1,6 +1,9 @@ rules: # Bronze - action-setup: done + action-setup: + status: exempt + comment: | + Integration has no actions. appropriate-polling: status: exempt comment: | @@ -10,7 +13,10 @@ rules: config-flow-test-coverage: done config-flow: done dependency-transparency: done - docs-actions: done + docs-actions: + status: exempt + comment: | + Integration has no actions. docs-conditions: status: exempt comment: This integration does not have any conditions. diff --git a/homeassistant/components/openai_conversation/services.yaml b/homeassistant/components/openai_conversation/services.yaml deleted file mode 100644 index 75fa097f25df26..00000000000000 --- a/homeassistant/components/openai_conversation/services.yaml +++ /dev/null @@ -1,60 +0,0 @@ -generate_image: - fields: - config_entry: - required: true - selector: - config_entry: - integration: openai_conversation - prompt: - required: true - selector: - text: - multiline: true - size: - required: false - example: "1024x1024" - default: "1024x1024" - selector: - select: - options: - - "1024x1024" - - "1024x1792" - - "1792x1024" - quality: - required: false - example: "standard" - default: "standard" - selector: - select: - options: - - "standard" - - "hd" - style: - required: false - example: "vivid" - default: "vivid" - selector: - select: - options: - - "vivid" - - "natural" -generate_content: - fields: - config_entry: - required: true - selector: - config_entry: - integration: openai_conversation - prompt: - required: true - selector: - text: - multiline: true - example: "Hello, how can I help you?" - filenames: - selector: - text: - multiline: true - example: | - - /path/to/file1.txt - - /path/to/file2.txt diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 1ab4611ca403ef..3ffc6bfb2d3532 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -208,20 +208,7 @@ } } }, - "exceptions": { - "invalid_config_entry": { - "message": "Invalid config entry provided. Got {config_entry}" - } - }, "issues": { - "deprecated_generate_content": { - "description": "Action 'openai_conversation.generate_content' is deprecated and will be removed in the 2026.9.0 release. Please use the 'ai_task.generate_data' action instead", - "title": "Deprecated 'generate_content' action" - }, - "deprecated_generate_image": { - "description": "Action 'openai_conversation.generate_image' is deprecated and will be removed in the 2026.9.0 release. Please use the 'ai_task.generate_image' action instead", - "title": "Deprecated 'generate_image' action" - }, "organization_verification_required": { "description": "Your organization must be verified to use this model. Please go to {platform_settings} and select Verify Organization. If you just verified, it can take up to 15 minutes for access to propagate.", "title": "Organization verification required" @@ -269,52 +256,5 @@ "medium": "[%key:common::state::medium%]" } } - }, - "services": { - "generate_content": { - "description": "Sends a conversational query to ChatGPT including any attached image or PDF files (deprecated)", - "fields": { - "config_entry": { - "description": "The config entry to use for this action", - "name": "Config entry" - }, - "filenames": { - "description": "List of files to upload", - "name": "Files" - }, - "prompt": { - "description": "The prompt to send", - "name": "Prompt" - } - }, - "name": "Generate content (deprecated)" - }, - "generate_image": { - "description": "Turns a prompt into an image (deprecated)", - "fields": { - "config_entry": { - "description": "The config entry to use for this action", - "name": "Config entry" - }, - "prompt": { - "description": "The text to turn into an image", - "example": "A photo of a dog", - "name": "Prompt" - }, - "quality": { - "description": "The quality of the image that will be generated", - "name": "Quality" - }, - "size": { - "description": "The size of the image to generate", - "name": "Size" - }, - "style": { - "description": "The style of the generated image", - "name": "Style" - } - }, - "name": "Generate image (deprecated)" - } } } diff --git a/tests/components/openai_conversation/test_entity.py b/tests/components/openai_conversation/test_entity.py index c24cb5b3d799b0..1297c2f90e2b24 100644 --- a/tests/components/openai_conversation/test_entity.py +++ b/tests/components/openai_conversation/test_entity.py @@ -1,10 +1,17 @@ """Tests for the OpenAI Conversation entity.""" +from pathlib import Path +from unittest.mock import patch + +import pytest import voluptuous as vol from homeassistant.components.openai_conversation.entity import ( _format_structured_output, + async_prepare_files_for_prompt, ) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector @@ -77,3 +84,83 @@ async def test_format_structured_output() -> None: "strict": True, "type": "object", } + + +@pytest.mark.parametrize( + ("filename", "expected_content"), + [ + pytest.param( + "image.jpg", + { + "type": "input_image", + "image_url": "data:image/jpeg;base64,QUJD", + "detail": "auto", + }, + id="jpeg", + ), + pytest.param( + "document.pdf", + { + "type": "input_file", + "filename": "document.pdf", + "file_data": "data:application/pdf;base64,QUJD", + }, + id="pdf", + ), + ], +) +async def test_prepare_files_for_prompt_infers_mime_type( + hass: HomeAssistant, + filename: str, + expected_content: dict[str, str], +) -> None: + """Test attachments without an explicit MIME type.""" + with ( + patch("pathlib.Path.exists", return_value=True), + patch("pathlib.Path.read_bytes", return_value=b"ABC"), + ): + assert await async_prepare_files_for_prompt(hass, [(Path(filename), None)]) == [ + expected_content + ] + + +@pytest.mark.parametrize( + ("filename", "exists", "mime_type", "error"), + [ + pytest.param("image.jpg", False, None, "does not exist", id="missing_file"), + pytest.param( + "document.txt", + True, + None, + "not an image file or PDF", + id="unsupported_inferred_mime_type", + ), + pytest.param( + "document.unknown_openai_attachment", + True, + None, + "not an image file or PDF", + id="unknown_mime_type", + ), + pytest.param( + "image.jpg", + True, + "text/plain", + "not an image file or PDF", + id="unsupported_explicit_mime_type", + ), + ], +) +async def test_prepare_files_for_prompt_invalid_file( + hass: HomeAssistant, + filename: str, + exists: bool, + mime_type: str | None, + error: str, +) -> None: + """Test missing files and unsupported attachment types.""" + with ( + patch("pathlib.Path.exists", return_value=exists), + pytest.raises(HomeAssistantError, match=error), + ): + await async_prepare_files_for_prompt(hass, [(Path(filename), mime_type)]) diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 4da106e583abfd..c572d10f627fa6 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,19 +1,11 @@ """Tests for the OpenAI integration.""" from typing import Any -from unittest.mock import AsyncMock, Mock, mock_open, patch +from unittest.mock import AsyncMock, patch import attr import httpx -from openai import ( - APIConnectionError, - AuthenticationError, - BadRequestError, - RateLimitError, -) -from openai.types.image import Image -from openai.types.images_response import ImagesResponse -from openai.types.responses import Response, ResponseOutputMessage, ResponseOutputText +from openai import APIConnectionError, AuthenticationError, BadRequestError import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props @@ -21,7 +13,6 @@ from homeassistant.components.openai_conversation import CONF_CHAT_MODEL from homeassistant.components.openai_conversation.const import ( CONF_REASONING_SUMMARY, - CONF_STORE_RESPONSES, DEFAULT_AI_TASK_NAME, DEFAULT_CONVERSATION_NAME, DEFAULT_STT_NAME, @@ -33,14 +24,12 @@ RECOMMENDED_TTS_OPTIONS, ) from homeassistant.config_entries import ( - SOURCE_REAUTH, ConfigEntryDisabler, ConfigEntryState, ConfigSubentryData, ) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryDisabler from homeassistant.helpers.entity_registry import RegistryEntryDisabler @@ -49,211 +38,6 @@ from tests.common import MockConfigEntry -@pytest.mark.parametrize( - ("service_data", "expected_args"), - [ - ( - {"prompt": "Picture of a dog"}, - { - "prompt": "Picture of a dog", - "size": "1024x1024", - "quality": "standard", - "style": "vivid", - }, - ), - ( - { - "prompt": "Picture of a dog", - "size": "1024x1792", - "quality": "hd", - "style": "vivid", - }, - { - "prompt": "Picture of a dog", - "size": "1024x1792", - "quality": "hd", - "style": "vivid", - }, - ), - ( - { - "prompt": "Picture of a dog", - "size": "1792x1024", - "quality": "standard", - "style": "natural", - }, - { - "prompt": "Picture of a dog", - "size": "1792x1024", - "quality": "standard", - "style": "natural", - }, - ), - ], -) -async def test_generate_image_service( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - service_data, - expected_args, -) -> None: - """Test generate image service.""" - service_data["config_entry"] = mock_config_entry.entry_id - expected_args["model"] = "dall-e-3" - expected_args["response_format"] = "url" - expected_args["n"] = 1 - - with patch( - "openai.resources.images.AsyncImages.generate", - new_callable=AsyncMock, - return_value=ImagesResponse( - created=1700000000, - data=[ - Image( - b64_json=None, - revised_prompt="A clear and detailed picture of an ordinary canine", - url="A", - ) - ], - ), - ) as mock_create: - response = await hass.services.async_call( - DOMAIN, - "generate_image", - service_data, - blocking=True, - return_response=True, - ) - - assert response == { - "url": "A", - "revised_prompt": "A clear and detailed picture of an ordinary canine", - } - assert len(mock_create.mock_calls) == 1 - assert mock_create.mock_calls[0][2] == expected_args - - -@pytest.mark.usefixtures("mock_init_component") -async def test_generate_image_service_error( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Test generate image service handles errors.""" - with ( - patch( - "openai.resources.images.AsyncImages.generate", - new_callable=AsyncMock, - side_effect=RateLimitError( - response=httpx.Response( - status_code=500, request=httpx.Request(method="GET", url="") - ), - body=None, - message="Reason", - ), - ), - pytest.raises(HomeAssistantError, match="Error generating image: Reason"), - ): - await hass.services.async_call( - DOMAIN, - "generate_image", - { - "config_entry": mock_config_entry.entry_id, - "prompt": "Image of an epic fail", - }, - blocking=True, - return_response=True, - ) - - with ( - patch( - "openai.resources.images.AsyncImages.generate", - new_callable=AsyncMock, - return_value=ImagesResponse( - created=1700000000, - data=[ - Image( - b64_json=None, - revised_prompt=None, - url=None, - ) - ], - ), - ), - pytest.raises(HomeAssistantError, match="No image returned"), - ): - await hass.services.async_call( - DOMAIN, - "generate_image", - { - "config_entry": mock_config_entry.entry_id, - "prompt": "Image of an epic fail", - }, - blocking=True, - return_response=True, - ) - - -@pytest.mark.usefixtures("mock_init_component") -async def test_generate_content_service_with_image_not_allowed_path( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Test generate content service with an image in a not allowed path.""" - with ( - patch("pathlib.Path.exists", return_value=True), - patch.object(hass.config, "is_allowed_path", return_value=False), - pytest.raises( - HomeAssistantError, - match=( - "Cannot read `doorbell_snapshot.jpg`, no access to path; " - "`allowlist_external_dirs` may need to be adjusted in " - "`configuration.yaml`" - ), - ), - ): - await hass.services.async_call( - DOMAIN, - "generate_content", - { - "config_entry": mock_config_entry.entry_id, - "prompt": "Describe this image from my doorbell camera", - "filenames": "doorbell_snapshot.jpg", - }, - blocking=True, - return_response=True, - ) - - -@pytest.mark.parametrize( - ("service_name", "error"), - [ - ("generate_image", "Invalid config entry provided. Got invalid_entry"), - ("generate_content", "Invalid config entry provided. Got invalid_entry"), - ], -) -async def test_invalid_config_entry( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - service_name: str, - error: str, -) -> None: - """Assert exception when invalid config entry is provided.""" - service_data = { - "prompt": "Picture of a dog", - "config_entry": "invalid_entry", - } - with pytest.raises(ServiceValidationError, match=error): - await hass.services.async_call( - DOMAIN, - service_name, - service_data, - blocking=True, - return_response=True, - ) - - @pytest.mark.parametrize( ("side_effect", "error"), [ @@ -313,332 +97,6 @@ async def test_init_auth_error( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR -@pytest.mark.parametrize("store_responses", [False, True]) -@pytest.mark.parametrize( - ("service_data", "expected_args", "number_of_files"), - [ - ( - {"prompt": "Picture of a dog", "filenames": []}, - { - "input": [ - { - "content": [ - { - "type": "input_text", - "text": "Picture of a dog", - }, - ], - }, - ], - }, - 0, - ), - ( - {"prompt": "Picture of a dog", "filenames": ["/a/b/c.pdf"]}, - { - "input": [ - { - "content": [ - { - "type": "input_text", - "text": "Picture of a dog", - }, - { - "type": "input_file", - "file_data": "data:application/pdf;base64,BASE64IMAGE1", - "filename": "/a/b/c.pdf", - }, - ], - }, - ], - }, - 1, - ), - ( - {"prompt": "Picture of a dog", "filenames": ["/a/b/c.jpg"]}, - { - "input": [ - { - "content": [ - { - "type": "input_text", - "text": "Picture of a dog", - }, - { - "type": "input_image", - "image_url": "data:image/jpeg;base64,BASE64IMAGE1", - "detail": "auto", - }, - ], - }, - ], - }, - 1, - ), - ( - { - "prompt": "Picture of a dog", - "filenames": ["/a/b/c.jpg", "d/e/f.jpg"], - }, - { - "input": [ - { - "content": [ - { - "type": "input_text", - "text": "Picture of a dog", - }, - { - "type": "input_image", - "image_url": "data:image/jpeg;base64,BASE64IMAGE1", - "detail": "auto", - }, - { - "type": "input_image", - "image_url": "data:image/jpeg;base64,BASE64IMAGE2", - "detail": "auto", - }, - ], - }, - ], - }, - 2, - ), - ], -) -async def test_generate_content_service( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - store_responses: bool, - service_data, - expected_args, - number_of_files, -) -> None: - """Test generate content service.""" - conversation_subentry = next( - sub - for sub in mock_config_entry.subentries.values() - if sub.subentry_type == "conversation" - ) - hass.config_entries.async_update_subentry( - mock_config_entry, - conversation_subentry, - data={**conversation_subentry.data, CONF_STORE_RESPONSES: store_responses}, - ) - await hass.async_block_till_done() - - service_data["config_entry"] = mock_config_entry.entry_id - expected_args["model"] = "gpt-4o-mini" - expected_args["max_output_tokens"] = 3000 - expected_args["top_p"] = 1.0 - expected_args["temperature"] = 1.0 - expected_args["user"] = None - expected_args["store"] = store_responses - expected_args["input"][0]["type"] = "message" - expected_args["input"][0]["role"] = "user" - - with ( - patch( - "openai.resources.responses.AsyncResponses.create", - new_callable=AsyncMock, - ) as mock_create, - patch( - "base64.b64encode", side_effect=[b"BASE64IMAGE1", b"BASE64IMAGE2"] - ) as mock_b64encode, - patch("pathlib.Path.read_bytes", Mock(return_value=b"ABC")) as mock_file, - patch("pathlib.Path.exists", return_value=True), - patch.object(hass.config, "is_allowed_path", return_value=True), - ): - mock_create.return_value = Response( - object="response", - id="resp_A", - created_at=1700000000, - model="gpt-4o-mini", - parallel_tool_calls=True, - tool_choice="auto", - tools=[], - output=[ - ResponseOutputMessage( - type="message", - id="msg_A", - content=[ - ResponseOutputText( - type="output_text", - text="This is the response", - annotations=[], - ) - ], - role="assistant", - status="completed", - ) - ], - ) - - response = await hass.services.async_call( - DOMAIN, - "generate_content", - service_data, - blocking=True, - return_response=True, - ) - assert response == {"text": "This is the response"} - assert len(mock_create.mock_calls) == 1 - assert mock_create.mock_calls[0][2] == expected_args - assert mock_b64encode.call_count == number_of_files - assert mock_file.call_count == number_of_files - - -@pytest.mark.parametrize( - ( - "service_data", - "error", - "exists_side_effect", - "is_allowed_side_effect", - ), - [ - ( - {"prompt": "Picture of a dog", "filenames": ["/a/b/c.jpg"]}, - "`/a/b/c.jpg` does not exist", - [False], - [True], - ), - ( - { - "prompt": "Picture of a dog", - "filenames": ["/a/b/c.jpg", "d/e/f.png"], - }, - "Cannot read `d/e/f.png`, no access to path;" - " `allowlist_external_dirs` may need to be adjusted" - " in `configuration.yaml`", - [True, True], - [True, False], - ), - ( - {"prompt": "Not a picture of a dog", "filenames": ["/a/b/c.mov"]}, - "Only images and PDF are supported by the OpenAI" - " API,`/a/b/c.mov` is not an image file or PDF", - [True], - [True], - ), - ], -) -async def test_generate_content_service_invalid( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, - service_data, - error, - exists_side_effect, - is_allowed_side_effect, -) -> None: - """Test generate content service.""" - service_data["config_entry"] = mock_config_entry.entry_id - - with ( - patch( - "openai.resources.responses.AsyncResponses.create", - new_callable=AsyncMock, - ) as mock_create, - patch("base64.b64encode", side_effect=[b"BASE64IMAGE1", b"BASE64IMAGE2"]), - patch("builtins.open", mock_open(read_data="ABC")), - patch("pathlib.Path.exists", side_effect=exists_side_effect), - patch.object( - hass.config, "is_allowed_path", side_effect=is_allowed_side_effect - ), - ): - with pytest.raises(HomeAssistantError, match=error): - await hass.services.async_call( - DOMAIN, - "generate_content", - service_data, - blocking=True, - return_response=True, - ) - assert len(mock_create.mock_calls) == 0 - - -@pytest.mark.usefixtures("mock_init_component") -async def test_generate_content_service_error( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, -) -> None: - """Test generate content service handles errors.""" - with ( - patch( - "openai.resources.responses.AsyncResponses.create", - side_effect=RateLimitError( - response=httpx.Response( - status_code=417, request=httpx.Request(method="GET", url="") - ), - body=None, - message="Reason", - ), - ), - pytest.raises(HomeAssistantError, match="Error generating content: Reason"), - ): - await hass.services.async_call( - DOMAIN, - "generate_content", - { - "config_entry": mock_config_entry.entry_id, - "prompt": "Image of an epic fail", - }, - blocking=True, - return_response=True, - ) - - -@pytest.mark.parametrize( - ("service_name", "patch_path"), - [ - ("generate_image", "openai.resources.images.AsyncImages.generate"), - ("generate_content", "openai.resources.responses.AsyncResponses.create"), - ], -) -@pytest.mark.usefixtures("mock_init_component") -async def test_service_auth_error( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - service_name: str, - patch_path: str, -) -> None: - """Test generate content service handles errors.""" - with ( - patch( - patch_path, - side_effect=AuthenticationError( - response=httpx.Response( - status_code=401, request=httpx.Request(method="GET", url="") - ), - body=None, - message="Reason", - ), - ), - pytest.raises(HomeAssistantError, match="Authentication error"), - ): - await hass.services.async_call( - DOMAIN, - service_name, - { - "config_entry": mock_config_entry.entry_id, - "prompt": "Image of an epic fail", - }, - blocking=True, - return_response=True, - ) - await hass.async_block_till_done() - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - - flow = flows[0] - assert flow["step_id"] == "reauth_confirm" - assert flow["handler"] == DOMAIN - assert "context" in flow - assert flow["context"]["source"] == SOURCE_REAUTH - assert flow["context"]["entry_id"] == mock_config_entry.entry_id - - async def test_migration_from_v1( hass: HomeAssistant, device_registry: dr.DeviceRegistry, From 7830a1f46695ae33225f1c0f0d99041051e9673f Mon Sep 17 00:00:00 2001 From: Abdellatif Anaflous <62770500+hktitof@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:14:42 +0100 Subject: [PATCH 10/19] Fix swallowed exceptions in mochad action handlers (#181869) --- homeassistant/components/mochad/light.py | 15 ++++++++---- homeassistant/components/mochad/strings.json | 10 ++++++++ homeassistant/components/mochad/switch.py | 15 ++++++++---- tests/components/mochad/test_light.py | 21 +++++++++++++++- tests/components/mochad/test_switch.py | 25 +++++++++++++++++++- 5 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/mochad/strings.json diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 8834521c0eaa1f..030a5107d2238b 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -15,6 +15,7 @@ ) from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -119,9 +120,12 @@ def turn_on(self, **kwargs: Any) -> None: self._adjust_brightness(brightness) self._attr_brightness = brightness self._attr_is_on = True - # pylint: disable-next=home-assistant-action-swallowed-exception except (MochadException, OSError) as exc: - _LOGGER.error("Error with mochad communication: %s", exc) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="turn_on_failed", + translation_placeholders={"error": str(exc)}, + ) from exc @override def turn_off(self, **kwargs: Any) -> None: @@ -138,6 +142,9 @@ def turn_off(self, **kwargs: Any) -> None: if self._brightness_levels == 31: self._attr_brightness = 0 self._attr_is_on = False - # pylint: disable-next=home-assistant-action-swallowed-exception except (MochadException, OSError) as exc: - _LOGGER.error("Error with mochad communication: %s", exc) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="turn_off_failed", + translation_placeholders={"error": str(exc)}, + ) from exc diff --git a/homeassistant/components/mochad/strings.json b/homeassistant/components/mochad/strings.json new file mode 100644 index 00000000000000..52698355a3c265 --- /dev/null +++ b/homeassistant/components/mochad/strings.json @@ -0,0 +1,10 @@ +{ + "exceptions": { + "turn_off_failed": { + "message": "Failed to turn off X10 device: {error}" + }, + "turn_on_failed": { + "message": "Failed to turn on X10 device: {error}" + } + } +} diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index 1b3cec9122672f..275404171e430b 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -10,6 +10,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -79,9 +80,12 @@ def turn_on(self, **kwargs: Any) -> None: if self._comm_type == "pl": self._controller.read_data() self._attr_is_on = True - # pylint: disable-next=home-assistant-action-swallowed-exception except (MochadException, OSError) as exc: - _LOGGER.error("Error with mochad communication: %s", exc) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="turn_on_failed", + translation_placeholders={"error": str(exc)}, + ) from exc @override def turn_off(self, **kwargs: Any) -> None: @@ -97,9 +101,12 @@ def turn_off(self, **kwargs: Any) -> None: if self._comm_type == "pl": self._controller.read_data() self._attr_is_on = False - # pylint: disable-next=home-assistant-action-swallowed-exception except (MochadException, OSError) as exc: - _LOGGER.error("Error with mochad communication: %s", exc) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="turn_off_failed", + translation_placeholders={"error": str(exc)}, + ) from exc def _get_device_status(self) -> bool: """Get the status of the switch from mochad.""" diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 49beebbaec6cb9..315f96e3cb3c5d 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -2,11 +2,13 @@ from unittest import mock +from pymochad.exceptions import MochadException import pytest from homeassistant.components import light -from homeassistant.components.mochad import light as mochad +from homeassistant.components.mochad import DOMAIN, light as mochad from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -65,3 +67,20 @@ async def test_turn_off(light_mock) -> None: """Test turn_off.""" light_mock.turn_off() light_mock.light.send_cmd.assert_called_once_with("off") + + +@pytest.mark.parametrize( + ("brightness", "action", "translation_key"), + [(32, "turn_on", "turn_on_failed"), (32, "turn_off", "turn_off_failed")], +) +async def test_action_raises_on_communication_error( + light_mock: mochad.MochadLight, action: str, translation_key: str +) -> None: + """Test that a failed action raises instead of being swallowed.""" + light_mock.light.send_cmd.side_effect = MochadException("boom") + with pytest.raises(HomeAssistantError) as exc_info: + getattr(light_mock, action)() + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == translation_key + assert "error" in exc_info.value.translation_placeholders diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index d7875246fac744..30218e6b8ce530 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -2,11 +2,13 @@ from unittest import mock +from pymochad.exceptions import MochadException import pytest from homeassistant.components import switch -from homeassistant.components.mochad import switch as mochad +from homeassistant.components.mochad import DOMAIN, switch as mochad from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from tests.common import MockEntityPlatform @@ -59,3 +61,24 @@ async def test_turn_off(switch_mock) -> None: """Test turn_off.""" switch_mock.turn_off() switch_mock.switch.send_cmd.assert_called_once_with("off") + + +@pytest.mark.parametrize( + ("action", "translation_key"), + [("turn_on", "turn_on_failed"), ("turn_off", "turn_off_failed")], +) +async def test_action_raises_on_communication_error( + switch_mock: mochad.MochadSwitch, action: str, translation_key: str +) -> None: + """Test that a failed action raises instead of being swallowed.""" + with mock.patch( + "homeassistant.components.mochad.switch.MochadException", + MochadException, + ): + switch_mock.switch.send_cmd.side_effect = MochadException("boom") + with pytest.raises(HomeAssistantError) as exc_info: + getattr(switch_mock, action)() + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == translation_key + assert "error" in exc_info.value.translation_placeholders From 6c0b4847f6e2740c83a3bb6d1243890c84100d5a Mon Sep 17 00:00:00 2001 From: Michael Rademaker Date: Sun, 13 Sep 2026 14:18:41 +0200 Subject: [PATCH 11/19] Bump earn-e-p1 to 0.3.0 (#182104) --- homeassistant/components/earn_e_p1/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/earn_e_p1/manifest.json b/homeassistant/components/earn_e_p1/manifest.json index 9adf4ade4662c8..a4218ce8da5f1e 100644 --- a/homeassistant/components/earn_e_p1/manifest.json +++ b/homeassistant/components/earn_e_p1/manifest.json @@ -15,5 +15,5 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["earn-e-p1==0.2.0"] + "requirements": ["earn-e-p1==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d976ee0f9f45d9..d0d38d5fa88ebd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -896,7 +896,7 @@ dynalite-panel==0.0.4 eagle100==0.1.1 # homeassistant.components.earn_e_p1 -earn-e-p1==0.2.0 +earn-e-p1==0.3.0 # homeassistant.components.easyenergy easyenergy==3.0.1 From 0e181a824a37b5115d48399bfee8b7844ef7d268 Mon Sep 17 00:00:00 2001 From: Michael Rademaker Date: Sun, 13 Sep 2026 14:24:31 +0200 Subject: [PATCH 12/19] Add L2 and L3 voltage and current sensors to EARN-E P1 Meter (#181833) Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> --- .../components/earn_e_p1/coordinator.py | 3 + homeassistant/components/earn_e_p1/sensor.py | 46 ++- .../components/earn_e_p1/strings.json | 8 +- tests/components/earn_e_p1/conftest.py | 10 +- .../earn_e_p1/snapshots/test_sensor.ambr | 264 ++++++++++++++++-- tests/components/earn_e_p1/test_sensor.py | 101 ++++++- 6 files changed, 401 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/earn_e_p1/coordinator.py b/homeassistant/components/earn_e_p1/coordinator.py index c4c61aaca14921..8fb48e51d67fc3 100644 --- a/homeassistant/components/earn_e_p1/coordinator.py +++ b/homeassistant/components/earn_e_p1/coordinator.py @@ -44,6 +44,7 @@ def __init__( self.mac = mac self.model: str | None = None self.sw_version: str | None = None + self.data_complete = False self._listener = listener def _handle_update(self, device: EarnEP1Device, _raw: dict[str, Any]) -> None: @@ -62,6 +63,8 @@ def _handle_update(self, device: EarnEP1Device, _raw: dict[str, Any]) -> None: model=self.model, sw_version=self.sw_version, ) + # Listeners run synchronously from async_set_updated_data and read this. + self.data_complete = device.data_complete self.async_set_updated_data(device.data) def start(self) -> None: diff --git a/homeassistant/components/earn_e_p1/sensor.py b/homeassistant/components/earn_e_p1/sensor.py index 8003f8f3d373dd..2d650b5715bc45 100644 --- a/homeassistant/components/earn_e_p1/sensor.py +++ b/homeassistant/components/earn_e_p1/sensor.py @@ -46,7 +46,26 @@ ), SensorEntityDescription( key="voltage_l1", - translation_key="voltage_l1", + translation_key="voltage_phase", + translation_placeholders={"phase": "1"}, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + ), + SensorEntityDescription( + key="voltage_l2", + translation_key="voltage_phase", + translation_placeholders={"phase": "2"}, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=0, + ), + SensorEntityDescription( + key="voltage_l3", + translation_key="voltage_phase", + translation_placeholders={"phase": "3"}, native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -54,7 +73,26 @@ ), SensorEntityDescription( key="current_l1", - translation_key="current_l1", + translation_key="current_phase", + translation_placeholders={"phase": "1"}, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="current_l2", + translation_key="current_phase", + translation_placeholders={"phase": "2"}, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="current_l3", + translation_key="current_phase", + translation_placeholders={"phase": "3"}, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -140,11 +178,11 @@ def _async_add_sensors() -> None: for description in SENSOR_DESCRIPTIONS if description.key in new_keys ) - if not pending_keys: + if not pending_keys or coordinator.data_complete: _async_remove_listener() _async_add_sensors() - if pending_keys: + if pending_keys and not coordinator.data_complete: remove_listener = coordinator.async_add_listener(_async_add_sensors) entry.async_on_unload(_async_remove_listener) diff --git a/homeassistant/components/earn_e_p1/strings.json b/homeassistant/components/earn_e_p1/strings.json index 903cf82b88df39..cb95b9fe3cee53 100644 --- a/homeassistant/components/earn_e_p1/strings.json +++ b/homeassistant/components/earn_e_p1/strings.json @@ -28,8 +28,8 @@ }, "entity": { "sensor": { - "current_l1": { - "name": "Current L1" + "current_phase": { + "name": "Current phase {phase}" }, "energy_exported_tariff1": { "name": "Energy exported tariff 1" @@ -52,8 +52,8 @@ "power_imported": { "name": "Power imported" }, - "voltage_l1": { - "name": "Voltage L1" + "voltage_phase": { + "name": "Voltage phase {phase}" }, "wifi_rssi": { "name": "Wi-Fi RSSI" diff --git a/tests/components/earn_e_p1/conftest.py b/tests/components/earn_e_p1/conftest.py index 6dcc1cdf671796..d0807c48ebff93 100644 --- a/tests/components/earn_e_p1/conftest.py +++ b/tests/components/earn_e_p1/conftest.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -from earn_e_p1 import EarnEP1Device +from earn_e_p1 import EarnEP1Device, PacketType import pytest from homeassistant.components.earn_e_p1.const import CONF_SERIAL, DOMAIN @@ -25,7 +25,11 @@ "power_delivered": 2.5, "power_returned": 0.0, "voltage_l1": 230.1, + "voltage_l2": 229.8, + "voltage_l3": 231.5, "current_l1": 10.87, + "current_l2": 8.2, + "current_l3": 4.35, "energy_delivered_tariff1": 12345.678, "energy_delivered_tariff2": 6789.012, "energy_returned_tariff1": 100.0, @@ -44,17 +48,21 @@ def trigger_callback( mock_listener: MagicMock, device_data: dict[str, Any] | None = None, + seen_packet_types: set[PacketType] | None = None, model: str | None = "P1 Meter", sw_version: str | None = "1.0.0", ) -> None: """Trigger the registered listener callback with device data.""" if device_data is None: device_data = MOCK_DEVICE_DATA + if seen_packet_types is None: + seen_packet_types = set(PacketType) callback = mock_listener.register.call_args[0][1] device = EarnEP1Device(host=MOCK_HOST, serial=MOCK_SERIAL) device.model = model device.sw_version = sw_version device.data = device_data + device.seen_packet_types = seen_packet_types callback(device, device_data) diff --git a/tests/components/earn_e_p1/snapshots/test_sensor.ambr b/tests/components/earn_e_p1/snapshots/test_sensor.ambr index b4a881a79c3557..3da2f42a116d57 100644 --- a/tests/components/earn_e_p1/snapshots/test_sensor.ambr +++ b/tests/components/earn_e_p1/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_sensor_platform[sensor.earn_e_p1_meter_current_l1-entry] +# name: test_sensor_platform[sensor.earn_e_p1_meter_current_phase_1-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -15,7 +15,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.earn_e_p1_meter_current_l1', + 'entity_id': 'sensor.earn_e_p1_meter_current_phase_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -23,7 +23,7 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Current L1', + 'object_id_base': 'Current phase 1', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 2, @@ -31,32 +31,148 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Current L1', + 'original_name': 'Current phase 1', 'platform': 'earn_e_p1', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'current_l1', + 'translation_key': 'current_phase', 'unique_id': 'E0012345678901234_current_l1', 'unit_of_measurement': , }) # --- -# name: test_sensor_platform[sensor.earn_e_p1_meter_current_l1-state] +# name: test_sensor_platform[sensor.earn_e_p1_meter_current_phase_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ : 'current', - : 'EARN-E P1 Meter Current L1', + : 'EARN-E P1 Meter Current phase 1', : , : , }), 'context': , - 'entity_id': 'sensor.earn_e_p1_meter_current_l1', + 'entity_id': 'sensor.earn_e_p1_meter_current_phase_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '10.87', }) # --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_current_phase_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.earn_e_p1_meter_current_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current phase 2', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 2', + 'platform': 'earn_e_p1', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase', + 'unique_id': 'E0012345678901234_current_l2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_current_phase_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'EARN-E P1 Meter Current phase 2', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.earn_e_p1_meter_current_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8.2', + }) +# --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_current_phase_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.earn_e_p1_meter_current_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current phase 3', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 3', + 'platform': 'earn_e_p1', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase', + 'unique_id': 'E0012345678901234_current_l3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_current_phase_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'current', + : 'EARN-E P1 Meter Current phase 3', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.earn_e_p1_meter_current_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.35', + }) +# --- # name: test_sensor_platform[sensor.earn_e_p1_meter_energy_exported_tariff_1-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -463,7 +579,7 @@ 'state': '2.5', }) # --- -# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_l1-entry] +# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_phase_1-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -479,7 +595,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.earn_e_p1_meter_voltage_l1', + 'entity_id': 'sensor.earn_e_p1_meter_voltage_phase_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -487,7 +603,7 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Voltage L1', + 'object_id_base': 'Voltage phase 1', 'options': dict({ 'sensor': dict({ 'suggested_display_precision': 0, @@ -495,32 +611,148 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Voltage L1', + 'original_name': 'Voltage phase 1', 'platform': 'earn_e_p1', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'voltage_l1', + 'translation_key': 'voltage_phase', 'unique_id': 'E0012345678901234_voltage_l1', 'unit_of_measurement': , }) # --- -# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_l1-state] +# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_phase_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ : 'voltage', - : 'EARN-E P1 Meter Voltage L1', + : 'EARN-E P1 Meter Voltage phase 1', : , : , }), 'context': , - 'entity_id': 'sensor.earn_e_p1_meter_voltage_l1', + 'entity_id': 'sensor.earn_e_p1_meter_voltage_phase_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '230.1', }) # --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_phase_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.earn_e_p1_meter_voltage_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Voltage phase 2', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 2', + 'platform': 'earn_e_p1', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_phase', + 'unique_id': 'E0012345678901234_voltage_l2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_phase_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'voltage', + : 'EARN-E P1 Meter Voltage phase 2', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.earn_e_p1_meter_voltage_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '229.8', + }) +# --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_phase_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.earn_e_p1_meter_voltage_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Voltage phase 3', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 3', + 'platform': 'earn_e_p1', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_phase', + 'unique_id': 'E0012345678901234_voltage_l3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_platform[sensor.earn_e_p1_meter_voltage_phase_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'voltage', + : 'EARN-E P1 Meter Voltage phase 3', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.earn_e_p1_meter_voltage_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '231.5', + }) +# --- # name: test_sensor_platform[sensor.earn_e_p1_meter_wi_fi_rssi-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/earn_e_p1/test_sensor.py b/tests/components/earn_e_p1/test_sensor.py index 268b09c821f501..8eb3872f43cc37 100644 --- a/tests/components/earn_e_p1/test_sensor.py +++ b/tests/components/earn_e_p1/test_sensor.py @@ -2,23 +2,45 @@ from unittest.mock import MagicMock +from earn_e_p1 import PacketType import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import trigger_callback +from .conftest import MOCK_DEVICE_DATA, trigger_callback from tests.common import MockConfigEntry, snapshot_platform -# A partial packet carries only the instantaneous values, without energy/gas. -PARTIAL_DEVICE_DATA = { +# The meter alternates between a realtime packet holding the instantaneous +# values and a heartbeat packet holding the meter totals. Each type only +# carries the keys the meter itself supports, so a single-phase meter never +# sends the L2/L3 keys and an electricity-only meter never sends gas. +REALTIME_3PHASE = { "power_delivered": 0.35, "power_returned": 0.0, "voltage_l1": 232.0, + "voltage_l2": 231.4, + "voltage_l3": 230.8, "current_l1": 2.0, + "current_l2": 1.5, + "current_l3": 1.1, } +REALTIME_1PHASE = { + "power_delivered": 0.35, + "power_returned": 0.0, + "voltage_l1": 232.0, + "current_l1": 2.0, +} +HEARTBEAT_NO_GAS = { + "energy_delivered_tariff1": 12345.678, + "energy_delivered_tariff2": 6789.012, + "energy_returned_tariff1": 100.0, + "energy_returned_tariff2": 50.0, + "wifiRSSI": -65, +} +HEARTBEAT = {**HEARTBEAT_NO_GAS, "gas_delivered": 1234.567} @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -75,12 +97,16 @@ async def test_sensors_added_when_key_appears_in_later_packet( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - trigger_callback(mock_listener, device_data=PARTIAL_DEVICE_DATA) + trigger_callback( + mock_listener, + device_data=REALTIME_3PHASE, + seen_packet_types={PacketType.REALTIME}, + ) await hass.async_block_till_done() assert len( er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id) - ) == len(PARTIAL_DEVICE_DATA) + ) == len(REALTIME_3PHASE) assert hass.states.get("sensor.earn_e_p1_meter_energy_imported_tariff_1") is None assert hass.states.get("sensor.earn_e_p1_meter_gas_consumed") is None @@ -90,7 +116,7 @@ async def test_sensors_added_when_key_appears_in_later_packet( entries = er.async_entries_for_config_entry( entity_registry, mock_config_entry.entry_id ) - assert len(entries) == 10 + assert len(entries) == len(MOCK_DEVICE_DATA) energy = hass.states.get("sensor.earn_e_p1_meter_energy_imported_tariff_1") assert energy is not None @@ -101,6 +127,69 @@ async def test_sensors_added_when_key_appears_in_later_packet( assert gas.state == "1234.567" +@pytest.mark.parametrize( + ("realtime", "heartbeat", "absent_entity_ids"), + [ + pytest.param( + REALTIME_1PHASE, + HEARTBEAT, + [ + "sensor.earn_e_p1_meter_voltage_phase_2", + "sensor.earn_e_p1_meter_voltage_phase_3", + "sensor.earn_e_p1_meter_current_phase_2", + "sensor.earn_e_p1_meter_current_phase_3", + ], + id="single_phase_meter", + ), + pytest.param( + REALTIME_3PHASE, + HEARTBEAT_NO_GAS, + ["sensor.earn_e_p1_meter_gas_consumed"], + id="no_gas_meter", + ), + ], +) +async def test_unsupported_keys_never_create_entities( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_listener: MagicMock, + entity_registry: er.EntityRegistry, + realtime: dict[str, float], + heartbeat: dict[str, float], + absent_entity_ids: list[str], +) -> None: + """Test keys a meter never sends do not become entities.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + trigger_callback( + mock_listener, + device_data=realtime, + seen_packet_types={PacketType.REALTIME}, + ) + await hass.async_block_till_done() + + trigger_callback(mock_listener, device_data={**realtime, **heartbeat}) + await hass.async_block_till_done() + + for entity_id in absent_entity_ids: + assert hass.states.get(entity_id) is None + + # Counted from the registry rather than the state machine, because the + # Wi-Fi RSSI sensor is disabled by default and so has no state. + assert len( + er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id) + ) == len(realtime) + len(heartbeat) + + trigger_callback(mock_listener) + await hass.async_block_till_done() + + # Both packet types have been seen, so the setup listener unsubscribed and + # a packet carrying the unsupported keys can no longer add them. + for entity_id in absent_entity_ids: + assert hass.states.get(entity_id) is None + + async def test_unload_after_all_sensors_added( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From d4466a8177e41daf2f938fc43e6868a7a6422f47 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 13 Sep 2026 14:27:13 +0200 Subject: [PATCH 13/19] Update Airly to Bronze (#181024) --- homeassistant/components/airly/manifest.json | 1 + homeassistant/components/airly/quality_scale.yaml | 2 +- script/hassfest/quality_scale.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index ccd37589e8c589..da8b34737ccab9 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -7,5 +7,6 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["airly"], + "quality_scale": "bronze", "requirements": ["airly==1.1.0"] } diff --git a/homeassistant/components/airly/quality_scale.yaml b/homeassistant/components/airly/quality_scale.yaml index ded9975693026d..ecd24f9e2c028e 100644 --- a/homeassistant/components/airly/quality_scale.yaml +++ b/homeassistant/components/airly/quality_scale.yaml @@ -20,7 +20,7 @@ rules: comment: This integration does not have any conditions. docs-high-level-description: done docs-installation-instructions: done - docs-removal-instructions: todo + docs-removal-instructions: done entity-event-setup: done entity-unique-id: done has-entity-name: done diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 9f356d29da0d02..9fde3bf51bb3a0 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -1034,7 +1034,6 @@ class Rule: "aemet", "aftership", "agent_dvr", - "airly", "airnow", "airq", "airthings", From ba84f9b506cea25548c148ee340c3fd58f003ba4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 13 Sep 2026 14:28:42 +0200 Subject: [PATCH 14/19] Update IQS rules list for Airly (#181924) --- homeassistant/components/airly/quality_scale.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/airly/quality_scale.yaml b/homeassistant/components/airly/quality_scale.yaml index ecd24f9e2c028e..d71dcb4c1cf384 100644 --- a/homeassistant/components/airly/quality_scale.yaml +++ b/homeassistant/components/airly/quality_scale.yaml @@ -37,9 +37,7 @@ rules: docs-configuration-parameters: status: exempt comment: The integration does not have an options flow. - docs-installation-parameters: - status: todo - comment: The documentation does not describe all setup parameters. + docs-installation-parameters: done entity-unavailable: done integration-owner: done log-when-unavailable: done From 5b42aaed9c7aa76be9908237d6d3da645d58fe8f Mon Sep 17 00:00:00 2001 From: Christo Date: Sun, 13 Sep 2026 20:39:35 +0800 Subject: [PATCH 15/19] Fix function name in external statistics mean_type message (#182031) --- homeassistant/components/recorder/statistics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 284beb18568684..1f7f41837f26ef 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -2883,7 +2883,7 @@ def async_add_external_statistics( if "mean_type" not in metadata and not _called_from_ws_api: # type: ignore[unreachable] report_usage( # type: ignore[unreachable] - "doesn't specify mean_type when calling async_import_statistics", + "doesn't specify mean_type when calling async_add_external_statistics", breaks_in_ha_version="2026.11", exclude_integrations={DOMAIN}, ) From ee14c3b56e494aa6f381e9eb78a388541aa3b132 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Sep 2026 14:40:24 +0200 Subject: [PATCH 16/19] Require the IRK in the Private BLE Device config flow (#181969) --- homeassistant/components/private_ble_device/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/private_ble_device/config_flow.py b/homeassistant/components/private_ble_device/config_flow.py index 02b3db494da1a4..2da8e4121b9727 100644 --- a/homeassistant/components/private_ble_device/config_flow.py +++ b/homeassistant/components/private_ble_device/config_flow.py @@ -70,7 +70,7 @@ async def async_step_user( data={CONF_IRK: irk_bytes.hex()}, ) - data_schema = vol.Schema({CONF_IRK: str}) + data_schema = vol.Schema({vol.Required(CONF_IRK): str}) return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) From eb5024a1f2dc77261add6ac5416e6bfafd4d497a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:00:07 -0400 Subject: [PATCH 17/19] Add quality scale file for vizio declaring platinum (#180066) --- homeassistant/components/vizio/manifest.json | 1 + .../components/vizio/quality_scale.yaml | 88 +++++++++++++++++++ script/hassfest/quality_scale.py | 2 - 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/vizio/quality_scale.yaml diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index ef908182efc03e..852d7ce22066da 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -7,6 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["vizaio"], + "quality_scale": "platinum", "requirements": ["vizaio==0.7.0"], "zeroconf": ["_viziocast._tcp.local."] } diff --git a/homeassistant/components/vizio/quality_scale.yaml b/homeassistant/components/vizio/quality_scale.yaml new file mode 100644 index 00000000000000..80abd81be32353 --- /dev/null +++ b/homeassistant/components/vizio/quality_scale.yaml @@ -0,0 +1,88 @@ +rules: + # Bronze + action-setup: + status: done + comment: | + The update_setting and send_text entity actions are registered from + async_setup in services.py, independent of config entry setup. + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: done + docs-conditions: + status: exempt + comment: This integration does not provide any conditions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + docs-triggers: + status: exempt + comment: This integration does not provide any triggers. + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: done + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: done + test-coverage: done + + # Gold + devices: done + diagnostics: done + discovery-update-info: done + discovery: done + docs-data-update: done + docs-examples: done + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done + dynamic-devices: + status: exempt + comment: | + A config entry represents a single SmartCast device, so there are no + additional devices that can be discovered after setup. + entity-category: done + entity-device-class: done + entity-disabled-by-default: + status: exempt + comment: | + The integration only creates a media player, a remote and, for + battery-powered devices, two diagnostic battery entities. All of them are + useful by default and none are noisy enough to warrant being disabled. + entity-translations: done + exception-translations: done + icon-translations: done + reconfiguration-flow: done + repair-issues: + status: exempt + comment: | + There are no user-repairable conditions. An unreachable or unauthenticated + device is surfaced through entity availability and the reauthentication + flow instead. + stale-devices: + status: exempt + comment: | + A config entry represents a single SmartCast device, so the device is + removed together with the config entry. + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: done diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 9fde3bf51bb3a0..ece0ac1c0fd9a8 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -953,7 +953,6 @@ class Rule: "viaggiatreno", "vilfo", "vivotek", - "vizio", "vlc_telnet", "voicerss", "voip", @@ -1911,7 +1910,6 @@ class Rule: "viaggiatreno", "vilfo", "vivotek", - "vizio", "vlc_telnet", "voicerss", "voip", From fdb882c737a91f6f0f9d0658d76a736b6271f0f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Sep 2026 11:22:56 -0400 Subject: [PATCH 18/19] Advertise required tool parameters in MCP Server (#182105) Co-authored-by: Claude --- homeassistant/components/mcp_server/server.py | 12 ++-- tests/components/mcp_server/conftest.py | 8 ++- tests/components/mcp_server/test_http.py | 66 +++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mcp_server/server.py b/homeassistant/components/mcp_server/server.py index 80d5169b1184b8..3806efb0242a11 100644 --- a/homeassistant/components/mcp_server/server.py +++ b/homeassistant/components/mcp_server/server.py @@ -45,13 +45,17 @@ def _format_tool( ) -> types.Tool: """Format tool specification.""" input_schema = to_openapi(tool.parameters, custom_serializer=custom_serializer) + mcp_schema: dict[str, Any] = { + "type": "object", + "properties": input_schema["properties"], + } + # Omitted by to_openapi when the tool has no required parameters. + if required := input_schema.get("required"): + mcp_schema["required"] = required return types.Tool( name=tool.name, description=tool.description or "", - inputSchema={ - "type": "object", - "properties": input_schema["properties"], - }, + inputSchema=mcp_schema, ) diff --git a/tests/components/mcp_server/conftest.py b/tests/components/mcp_server/conftest.py index e318713cf7bfdb..00a8dda93b9bfd 100644 --- a/tests/components/mcp_server/conftest.py +++ b/tests/components/mcp_server/conftest.py @@ -1,6 +1,7 @@ """Common fixtures for the Model Context Protocol Server tests.""" from collections.abc import Generator +from dataclasses import dataclass, field from unittest.mock import AsyncMock, patch import pytest @@ -16,8 +17,11 @@ TEST_LLM_API_ID = "test-api" +@dataclass(slots=True, kw_only=True) class MockLLMAPI(llm.API): - """Test LLM API that does not expose any tools.""" + """Test LLM API that exposes the tools it is created with.""" + + tools: list[llm.Tool] = field(default_factory=list) async def async_get_api_instance( self, llm_context: llm.LLMContext @@ -27,7 +31,7 @@ async def async_get_api_instance( api=self, api_prompt="Test prompt", llm_context=llm_context, - tools=[], + tools=self.tools, ) diff --git a/tests/components/mcp_server/test_http.py b/tests/components/mcp_server/test_http.py index 9ba896c7971f97..f81a7b5fa75585 100644 --- a/tests/components/mcp_server/test_http.py +++ b/tests/components/mcp_server/test_http.py @@ -15,6 +15,7 @@ import mcp.client.streamable_http from mcp.shared.exceptions import McpError import pytest +import voluptuous as vol from homeassistant.components.conversation import DOMAIN as CONVERSATION_DOMAIN from homeassistant.components.homeassistant.exposed_entities import async_expose_entity @@ -42,6 +43,7 @@ ) from homeassistant.helpers.httpx_client import create_async_httpx_client from homeassistant.setup import async_setup_component +from homeassistant.util.json import JsonObjectType from .conftest import TEST_LLM_API_ID, MockLLMAPI @@ -80,6 +82,25 @@ """ +class _StubTool(llm.Tool): + """Minimal tool with a configurable parameter schema.""" + + name = "test_tool" + + def __init__(self, parameters: vol.Schema) -> None: + """Initialize the stub tool.""" + self.parameters = parameters + + async def async_call( + self, + hass: HomeAssistant, + tool_input: llm.ToolInput, + llm_context: llm.LLMContext, + ) -> JsonObjectType: + """Return an empty result.""" + return {} + + @pytest.fixture async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Set up the config entry.""" @@ -612,6 +633,51 @@ async def test_mcp_tools_list( assert properties.get("name") == {"type": "string"} +@pytest.mark.parametrize("llm_hass_api", [TEST_LLM_API_ID]) +@pytest.mark.parametrize( + ("parameters", "expected_required"), + [ + pytest.param( + vol.Schema({vol.Required("name"): str, vol.Optional("area"): str}), + ["name"], + id="required-and-optional", + ), + pytest.param( + vol.Schema({vol.Optional("area"): str}), + None, + id="optional-only", + ), + ], +) +async def test_mcp_tools_list_required_parameters( + hass: HomeAssistant, + setup_integration: None, + mcp_url: str, + mcp_client: MCPClientFactory, + hass_supervisor_access_token: str, + parameters: vol.Schema, + expected_required: list[str] | None, +) -> None: + """Test the tools list advertises the required tool parameters.""" + + llm.async_register_api( + hass, + MockLLMAPI( + hass=hass, + id=TEST_LLM_API_ID, + name="Test API", + tools=[_StubTool(parameters)], + ), + ) + + async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session: + result = await session.list_tools() + + tool = next(iter(tool for tool in result.tools if tool.name == "test_tool")) + assert tool.inputSchema.get("type") == "object" + assert tool.inputSchema.get("required") == expected_required + + @pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST, STATELESS_LLM_API]) async def test_mcp_tool_call( hass: HomeAssistant, From 743b1489e196830f8b692267cc25c4b3df6aff7a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Sep 2026 17:24:06 +0200 Subject: [PATCH 19/19] Use probatio directly in tests, scripts and pylint plugins (#182113) --- .../checkers/config_flow/no_name.py | 2 +- .../checkers/config_flow/no_polling.py | 2 +- .../checkers/flow_translations.py | 6 +- .../helpers/ast_utils.py | 4 +- script/hassfest/brand.py | 18 +- script/hassfest/conditions.py | 50 +-- script/hassfest/icons.py | 86 ++-- script/hassfest/manifest.py | 181 ++++---- script/hassfest/quality_scale.py | 26 +- script/hassfest/services.py | 74 ++-- script/hassfest/translations.py | 308 +++++++------- script/hassfest/triggers.py | 50 +-- .../config_flow/integration/config_flow.py | 10 +- .../integration/config_flow.py | 10 +- .../tests/test_config_flow.py | 2 +- .../integration/device_action.py | 6 +- .../integration/device_condition.py | 6 +- .../integration/device_trigger.py | 6 +- .../integration/integration/__init__.py | 6 +- tests/auth/permissions/test_entities.py | 10 +- tests/auth/providers/test_homeassistant.py | 4 +- tests/auth/providers/test_trusted_networks.py | 22 +- tests/auth/test_init.py | 6 +- tests/common.py | 16 +- tests/components/ai_task/test_entity.py | 10 +- tests/components/ai_task/test_init.py | 24 +- tests/components/airos/test_config_flow.py | 6 +- .../components/amberelectric/test_services.py | 4 +- tests/components/anthropic/test_ai_task.py | 26 +- .../components/anthropic/test_conversation.py | 14 +- tests/components/api/test_init.py | 4 +- .../assist_pipeline/test_pipeline.py | 4 +- .../assist_satellite/test_entity.py | 4 +- tests/components/automation/test_init.py | 14 +- .../azure_data_explorer/test_config_flow.py | 6 +- .../bang_olufsen/test_media_player.py | 2 +- tests/components/bayesian/test_config_flow.py | 14 +- .../components/blackbird/test_media_player.py | 4 +- tests/components/blue_current/test_init.py | 2 +- tests/components/blueprint/test_schemas.py | 6 +- tests/components/bosch_alarm/test_services.py | 8 +- tests/components/broadlink/test_helpers.py | 4 +- tests/components/bsblan/test_config_flow.py | 4 +- tests/components/bsblan/test_services.py | 4 +- tests/components/calendar/test_init.py | 42 +- tests/components/climate/test_init.py | 4 +- tests/components/climate/test_trigger.py | 8 +- tests/components/cloud/test_ai_task.py | 6 +- tests/components/cloud/test_entity.py | 12 +- tests/components/cloud/test_tts.py | 6 +- .../color_extractor/test_services.py | 2 +- tests/components/common.py | 6 +- .../components/config/test_config_entries.py | 76 ++-- tests/components/conftest.py | 4 +- .../components/conversation/test_chat_log.py | 18 +- tests/components/conversation/test_init.py | 4 +- tests/components/conversation/test_trigger.py | 10 +- tests/components/deconz/test_services.py | 4 +- tests/components/demo/test_climate.py | 14 +- tests/components/demo/test_humidifier.py | 4 +- tests/components/demo/test_media_player.py | 12 +- tests/components/demo/test_number.py | 4 +- tests/components/demo/test_water_heater.py | 8 +- .../components/device_automation/test_init.py | 27 +- tests/components/dlna_dmr/test_config_flow.py | 2 +- tests/components/downloader/test_services.py | 10 +- tests/components/dynalite/test_init.py | 2 +- tests/components/eafm/test_config_flow.py | 2 +- tests/components/easyenergy/test_services.py | 12 +- tests/components/ecobee/test_util.py | 6 +- tests/components/energy/test_data.py | 45 +- tests/components/energyzero/test_services.py | 12 +- .../environment_canada/test_config_flow.py | 6 +- tests/components/esphome/test_manager.py | 4 +- .../eurotronic_cometblue/test_config_flow.py | 4 +- .../eurotronic_cometblue/test_services.py | 14 +- tests/components/flo/test_services.py | 2 +- tests/components/fritz/test_services.py | 2 +- tests/components/frontend/test_init.py | 12 +- .../generic_hygrostat/test_humidifier.py | 4 +- .../generic_thermostat/test_climate.py | 4 +- tests/components/google/test_init.py | 26 +- .../test_ai_task.py | 8 +- tests/components/google_mail/test_notify.py | 6 +- .../green_planet_energy/test_services.py | 4 +- .../growatt_server/test_config_flow.py | 6 +- tests/components/hassio/test_init.py | 2 +- tests/components/hdmi_cec/test_init.py | 10 +- tests/components/history_stats/test_sensor.py | 4 +- .../components/home_connect/test_services.py | 2 +- tests/components/homeassistant/test_init.py | 4 +- tests/components/homeassistant/test_scene.py | 4 +- .../homeassistant/triggers/test_event.py | 4 +- .../triggers/test_numeric_state.py | 8 +- .../homeassistant/triggers/test_time.py | 8 +- .../triggers/test_time_pattern.py | 4 +- .../test_silabs_multiprotocol_addon.py | 2 +- tests/components/homekit/test_config_flow.py | 6 +- tests/components/homekit/test_util.py | 4 +- tests/components/http/test_data_validator.py | 9 +- tests/components/http/test_view.py | 4 +- tests/components/hue/test_config_flow.py | 6 +- tests/components/humidifier/test_condition.py | 6 +- tests/components/humidifier/test_trigger.py | 6 +- .../husqvarna_automower/test_lawn_mower.py | 2 +- .../components/idrive_e2/test_config_flow.py | 4 +- tests/components/imap/test_config_flow.py | 4 +- tests/components/influxdb/test_sensor.py | 2 +- tests/components/input_datetime/test_init.py | 8 +- tests/components/input_number/test_init.py | 4 +- tests/components/izone/test_climate.py | 4 +- tests/components/jellyfin/test_config_flow.py | 2 +- .../keenetic_ndms2/test_config_flow.py | 4 +- tests/components/kiosker/test_services.py | 4 +- tests/components/kitchen_sink/test_init.py | 4 +- tests/components/knx/test_diagnostic.py | 2 +- tests/components/labs/test_websocket_api.py | 2 +- tests/components/light/test_init.py | 22 +- tests/components/logbook/test_init.py | 4 +- tests/components/lovelace/test_init.py | 4 +- tests/components/lutron/test_config_flow.py | 2 +- tests/components/mcp/test_init.py | 6 +- tests/components/media_player/test_init.py | 6 +- tests/components/melnor/test_config_flow.py | 6 +- tests/components/miele/test_services.py | 2 +- tests/components/modbus/test_init.py | 24 +- tests/components/monzo/test_services.py | 4 +- tests/components/moon/test_condition.py | 4 +- tests/components/mopeka/test_config_flow.py | 4 +- tests/components/motioneye/test_camera.py | 10 +- tests/components/mqtt/common.py | 4 +- tests/components/mqtt/test_climate.py | 10 +- tests/components/mqtt/test_config_flow.py | 10 +- tests/components/mqtt/test_fan.py | 2 +- tests/components/mqtt/test_humidifier.py | 2 +- tests/components/mqtt/test_init.py | 68 +-- tests/components/mqtt/test_water_heater.py | 4 +- .../music_assistant/test_media_player.py | 4 +- tests/components/mysensors/test_gateway.py | 4 +- tests/components/netatmo/test_climate.py | 2 +- tests/components/notify/test_init.py | 6 +- tests/components/notify/test_legacy.py | 4 +- tests/components/ntfy/test_services.py | 8 +- tests/components/obihai/__init__.py | 2 +- tests/components/ollama/test_ai_task.py | 10 +- tests/components/ollama/test_conversation.py | 12 +- tests/components/onedrive/test_services.py | 4 +- tests/components/open_router/test_ai_task.py | 10 +- .../openai_conversation/test_ai_task.py | 10 +- .../openai_conversation/test_entity.py | 10 +- tests/components/opendisplay/test_services.py | 4 +- tests/components/openuv/test_config_flow.py | 6 +- tests/components/orvibo/test_config_flow.py | 4 +- tests/components/peblar/test_services.py | 4 +- tests/components/peco/test_config_flow.py | 2 +- tests/components/pilight/test_init.py | 2 +- tests/components/portainer/test_services.py | 2 +- tests/components/random/test_config_flow.py | 2 +- tests/components/recorder/test_purge.py | 2 +- tests/components/recorder/test_statistics.py | 4 +- .../remember_the_milk/test_config_flow.py | 4 +- .../components/repairs/test_websocket_api.py | 6 +- tests/components/rflink/test_init.py | 2 +- tests/components/risco/test_config_flow.py | 6 +- tests/components/schlage/test_lock.py | 4 +- tests/components/select/test_condition.py | 6 +- tests/components/sensibo/test_climate.py | 2 +- tests/components/sharkiq/test_vacuum.py | 2 +- .../signal_messenger/test_notify.py | 10 +- tests/components/sofar/test_services.py | 4 +- tests/components/sql/test_init.py | 16 +- tests/components/sql/test_services.py | 9 +- tests/components/sql/test_util.py | 4 +- tests/components/statsd/test_init.py | 6 +- tests/components/subaru/test_lock.py | 2 +- tests/components/sun/test_condition.py | 8 +- tests/components/sun/test_trigger.py | 8 +- .../swiss_public_transport/test_services.py | 8 +- .../switchbot_cloud/test_services.py | 4 +- tests/components/tasmota/test_fan.py | 2 +- tests/components/template/conftest.py | 4 +- tests/components/template/test_config.py | 6 +- tests/components/template/test_fan.py | 6 +- .../components/teslemetry/test_config_flow.py | 4 +- tests/components/test_shared_test_helpers.py | 6 +- .../components/time_date/test_config_flow.py | 4 +- tests/components/timer/test_trigger.py | 4 +- tests/components/todo/test_init.py | 12 +- .../components/tomato/test_device_tracker.py | 12 +- .../components/tplink_omada/test_services.py | 4 +- tests/components/tts/common.py | 8 +- .../components/universal/test_media_player.py | 2 +- tests/components/velbus/test_services.py | 4 +- tests/components/wake_on_lan/test_init.py | 4 +- tests/components/water_heater/test_init.py | 4 +- .../components/websocket_api/test_commands.py | 14 +- .../websocket_api/test_connection.py | 6 +- .../websocket_api/test_decorators.py | 4 +- tests/components/websocket_api/test_init.py | 8 +- tests/components/zha/test_websocket_api.py | 4 +- tests/components/zone/test_condition.py | 4 +- tests/components/zone/test_trigger.py | 4 +- tests/components/zoneminder/test_services.py | 4 +- tests/components/zoneminder/test_switch.py | 6 +- tests/components/zwave_js/test_condition.py | 10 +- tests/components/zwave_js/test_config_flow.py | 2 +- .../zwave_js/test_config_validation.py | 4 +- .../zwave_js/test_credential_services.py | 8 +- .../zwave_js/test_device_condition.py | 13 +- tests/components/zwave_js/test_fan.py | 2 +- tests/components/zwave_js/test_helpers.py | 4 +- tests/components/zwave_js/test_services.py | 22 +- tests/components/zwave_js/test_trigger.py | 20 +- tests/hassfest/test_translations.py | 12 +- tests/hassfest/test_version.py | 6 +- tests/helpers/template/test_init.py | 10 +- tests/helpers/test_automation.py | 16 +- tests/helpers/test_check_config.py | 8 +- tests/helpers/test_collection.py | 10 +- tests/helpers/test_condition.py | 20 +- tests/helpers/test_config_validation.py | 392 +++++++++--------- tests/helpers/test_device.py | 6 +- tests/helpers/test_entity.py | 8 +- tests/helpers/test_entity_component.py | 16 +- tests/helpers/test_entity_platform.py | 12 +- tests/helpers/test_entity_registry.py | 6 +- tests/helpers/test_intent.py | 10 +- tests/helpers/test_llm.py | 40 +- tests/helpers/test_reload.py | 4 +- .../helpers/test_schema_config_entry_flow.py | 58 +-- tests/helpers/test_script.py | 10 +- tests/helpers/test_selector.py | 56 +-- tests/helpers/test_service.py | 20 +- tests/helpers/test_trigger.py | 94 +++-- .../actions/test_swallowed_exceptions.py | 4 +- tests/pylint/config_flow/test_no_name.py | 26 +- tests/pylint/config_flow/test_no_polling.py | 26 +- .../test_serial_port_usb_dependency.py | 10 +- tests/pylint/test_flow_translations.py | 152 +++---- tests/syrupy.py | 7 +- tests/test_config.py | 100 +++-- tests/test_config_entries.py | 4 +- tests/test_core.py | 4 +- tests/test_core_config.py | 2 +- tests/test_data_entry_flow.py | 60 +-- tests/test_setup.py | 6 +- tests/util/test_color.py | 8 +- tests/util/yaml/test_init.py | 12 +- 248 files changed, 1851 insertions(+), 1714 deletions(-) diff --git a/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_name.py b/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_name.py index f9fcc8ac32b75c..2cc445fdb2fc11 100644 --- a/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_name.py +++ b/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_name.py @@ -46,7 +46,7 @@ class HassEnforceConfigFlowNoNameChecker(BaseChecker): options = () def visit_call(self, node: nodes.Call) -> None: - """Check for name fields in vol.Required/Optional calls.""" + """Check for name fields in probatio.Required/Optional calls.""" parsed = parse_module(node.root().name) if parsed is None or parsed.module != Module.CONFIG_FLOW: return diff --git a/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_polling.py b/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_polling.py index b9f07d622d79a6..21d755c7f9a873 100644 --- a/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_polling.py +++ b/pylint/plugins/pylint_home_assistant/checkers/config_flow/no_polling.py @@ -52,7 +52,7 @@ class HassEnforceConfigFlowNoPollingChecker(BaseChecker): options = () def visit_call(self, node: nodes.Call) -> None: - """Check for polling interval fields in vol.Required/Optional calls.""" + """Check for polling interval fields in probatio.Required/Optional calls.""" parsed = parse_module(node.root().name) if parsed is None or parsed.module != Module.CONFIG_FLOW: return diff --git a/pylint/plugins/pylint_home_assistant/checkers/flow_translations.py b/pylint/plugins/pylint_home_assistant/checkers/flow_translations.py index cae4dcb0b38c19..2d4690869d5814 100644 --- a/pylint/plugins/pylint_home_assistant/checkers/flow_translations.py +++ b/pylint/plugins/pylint_home_assistant/checkers/flow_translations.py @@ -124,7 +124,7 @@ def _extract_items_from_node( items.append(_Field(name)) return items - # SCHEMA_VAR.schema - the wrapped dict of a voluptuous Schema + # SCHEMA_VAR.schema - the wrapped dict of a probatio Schema if isinstance(node, nodes.Attribute) and node.attrname == "schema": return _extract_items_from_node(node.expr) @@ -169,7 +169,7 @@ def _extract_items_from_call(node: nodes.Call) -> list[_Field | _Section] | None *_extract_items_from_node(node.func.expr), *_extract_items_from_node(node.args[0]), ] - # vol.Schema({...}) / Schema({...}) - first arg is the schema dict + # probatio.Schema({...}) / Schema({...}) - first arg is the schema dict case nodes.Attribute(attrname="Schema") | nodes.Name(name="Schema"): return _extract_items_from_node(node.args[0]) # self.add_suggested_values_to_schema(schema, ...) - first arg is @@ -213,7 +213,7 @@ def _extract_section_fields(node: nodes.NodeNG) -> list[str] | None: if not node.args: return None - # section(vol.Schema({...}), ...) - first arg is the schema + # section(probatio.Schema({...}), ...) - first arg is the schema inner_items = _extract_items_from_node(node.args[0]) return [item.name for item in inner_items if isinstance(item, _Field)] diff --git a/pylint/plugins/pylint_home_assistant/helpers/ast_utils.py b/pylint/plugins/pylint_home_assistant/helpers/ast_utils.py index 5a2d7d6d2fe689..c5a9180f218d8b 100644 --- a/pylint/plugins/pylint_home_assistant/helpers/ast_utils.py +++ b/pylint/plugins/pylint_home_assistant/helpers/ast_utils.py @@ -68,10 +68,10 @@ def enclosing_function(node: nodes.NodeNG) -> nodes.FunctionDef | None: def get_schema_field_name(node: nodes.Call) -> str | None: - """Extract the field name from ``vol.Required(...)`` or ``vol.Optional(...)``. + """Extract the field name from ``probatio.Required(...)`` or ``probatio.Optional(...)``. Returns the string field name (either a literal or a ``Name`` identifier), - or ``None`` if *node* is not a voluptuous schema field call. + or ``None`` if *node* is not a probatio schema field call. """ match node: case nodes.Call( diff --git a/script/hassfest/brand.py b/script/hassfest/brand.py index d0967f909d9f99..ebaf8a56013fac 100644 --- a/script/hassfest/brand.py +++ b/script/hassfest/brand.py @@ -1,17 +1,17 @@ """Brand validation.""" -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error from .model import Brand, Config, Integration -BRAND_SCHEMA = vol.Schema( +BRAND_SCHEMA = probatio.Schema( { - vol.Required("domain"): str, - vol.Required("name"): str, - vol.Optional("integrations"): [str], - vol.Optional("iot_standards"): [ - vol.Any("homekit", "matter", "zigbee", "zwave") + probatio.Required("domain"): str, + probatio.Required("name"): str, + probatio.Optional("integrations"): [str], + probatio.Optional("iot_standards"): [ + probatio.Any("homekit", "matter", "zigbee", "zwave") ], } ) @@ -25,7 +25,7 @@ def _validate_brand( """Validate brand file.""" try: BRAND_SCHEMA(brand.brand) - except vol.Invalid as err: + except probatio.Invalid as err: config.add_error( "brand", f"Invalid brand file {brand.path.name}: {humanize_error(brand.brand, err)}", diff --git a/script/hassfest/conditions.py b/script/hassfest/conditions.py index efe71fcb7a73af..6ea2b49a73eac1 100644 --- a/script/hassfest/conditions.py +++ b/script/hassfest/conditions.py @@ -6,8 +6,8 @@ import re from typing import Any -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error from homeassistant.const import CONF_SELECTOR from homeassistant.exceptions import HomeAssistantError @@ -20,7 +20,7 @@ def exists(value: Any) -> Any: """Check if value exists.""" if value is None: - raise vol.Invalid("Value cannot be None") + raise probatio.Invalid("Value cannot be None") return value @@ -31,13 +31,15 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: # Validate context if present if "context" in field_schema: if CONF_SELECTOR not in field_schema: - raise vol.Invalid( + raise probatio.Invalid( f"Context defined without a selector in '{field_name}'" ) context = field_schema["context"] if not isinstance(context, dict): - raise vol.Invalid(f"Context must be a dictionary in '{field_name}'") + raise probatio.Invalid( + f"Context must be a dictionary in '{field_name}'" + ) # Determine which selector type is being used selector_config = field_schema[CONF_SELECTOR] @@ -50,7 +52,7 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: allowed = ( ", ".join(sorted(allowed_keys)) if allowed_keys else "none" ) - raise vol.Invalid( + raise probatio.Invalid( f"Invalid context key '{context_key}'" f" for selector type" f" '{selector_class.selector_type}'." @@ -59,7 +61,7 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: # Check if the referenced field exists in condition schema or target if not isinstance(field_ref, str): - raise vol.Invalid( + raise probatio.Invalid( f"Context value for '{context_key}'" " must be a string field reference" ) @@ -76,7 +78,7 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: ): allowed_types = ", ".join(allowed_keys.get(context_key, set())) sel_type = field_selector_class.selector_type - raise vol.Invalid( + raise probatio.Invalid( f"The context '{context_key}' for" f" '{field_name}' references" f" '{field_ref}', but" @@ -91,7 +93,7 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: context_key, set() ): allowed_types = ", ".join(allowed_keys.get(context_key, set())) - raise vol.Invalid( + raise probatio.Invalid( f"The context '{context_key}' for" f" '{field_name}' references" f" 'target', but '{context_key}'" @@ -100,7 +102,7 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: ) if not field_exists: - raise vol.Invalid( + raise probatio.Invalid( f"Context reference '{field_ref}'" f" for key '{context_key}' does" " not exist in condition schema" @@ -110,24 +112,24 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: return condition_schema -FIELD_SCHEMA = vol.Schema( +FIELD_SCHEMA = probatio.Schema( { - vol.Optional("example"): exists, - vol.Optional("default"): exists, - vol.Optional("required"): bool, - vol.Optional(CONF_SELECTOR): selector.validate_selector, + probatio.Optional("example"): exists, + probatio.Optional("default"): exists, + probatio.Optional("required"): bool, + probatio.Optional(CONF_SELECTOR): selector.validate_selector, # key is context key, value is field name in schema # Validated in validate_field_schema - vol.Optional("context"): {str: str}, + probatio.Optional("context"): {str: str}, } ) -CONDITION_SCHEMA = vol.Any( - vol.All( - vol.Schema( +CONDITION_SCHEMA = probatio.Any( + probatio.All( + probatio.Schema( { - vol.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA, - vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), + probatio.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA, + probatio.Optional("fields"): probatio.Schema({str: FIELD_SCHEMA}), } ), validate_field_schema, @@ -135,9 +137,9 @@ def validate_field_schema(condition_schema: dict[str, Any]) -> dict[str, Any]: None, ) -CONDITIONS_SCHEMA = vol.Schema( +CONDITIONS_SCHEMA = probatio.Schema( { - vol.Remove(vol.All(str, condition.starts_with_dot)): object, + probatio.Remove(probatio.All(str, condition.starts_with_dot)): object, cv.underscore_slug: CONDITION_SCHEMA, } ) @@ -186,7 +188,7 @@ def validate_conditions(config: Config, integration: Integration) -> None: # no try: conditions = CONDITIONS_SCHEMA(data) - except vol.Invalid as err: + except probatio.Invalid as err: integration.add_error( "conditions", f"Invalid conditions.yaml: {humanize_error(data, err)}" ) diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index 659e2818c82b02..5567edeb5320cf 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -3,8 +3,8 @@ from typing import Any import orjson -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import convert_shorthand_service_icon @@ -17,7 +17,7 @@ def icon_value_validator(value: Any) -> str: """Validate that the icon is a valid icon.""" value = cv.string_with_no_html(value) if not value.startswith("mdi:"): - raise vol.Invalid( + raise probatio.Invalid( "The icon needs to be a valid icon from Material" " Design Icons and start with `mdi:`" ) @@ -29,7 +29,9 @@ def range_key_validator(value: str) -> str: try: float(value) except (TypeError, ValueError) as err: - raise vol.Invalid(f"Invalid range key '{value}', needs to be numeric.") from err + raise probatio.Invalid( + f"Invalid range key '{value}', needs to be numeric." + ) from err return value @@ -37,7 +39,7 @@ def range_key_validator(value: str) -> str: def require_default_icon_validator(value: dict) -> dict: """Validate that a default icon is set.""" if "_" not in value: - raise vol.Invalid( + raise probatio.Invalid( "An entity component needs to have a default icon defined with `_`" ) return value @@ -49,7 +51,7 @@ def ensure_not_same_as_default(value: dict) -> dict: if (default := section.get("default")) and (states := section.get("state")): for state, icon in states.items(): if icon == default: - raise vol.Invalid( + raise probatio.Invalid( f"The icon for state `{translation_key}.{state}` is the" " same as the default icon and thus can be removed" ) @@ -65,19 +67,19 @@ def ensure_range_is_sorted(value: dict) -> dict: try: range_values = [float(key) for key in ranges] except ValueError as err: - raise vol.Invalid( + raise probatio.Invalid( f"Range values for `{section_key}` must be numeric" ) from err if range_values != sorted(range_values): - raise vol.Invalid( + raise probatio.Invalid( f"Range values for `{section_key}` must be in ascending order" ) return value -DATA_ENTRY_ICONS_SCHEMA = vol.Schema( +DATA_ENTRY_ICONS_SCHEMA = probatio.Schema( { "step": { str: { @@ -91,10 +93,10 @@ def ensure_range_is_sorted(value: dict) -> dict: CORE_SERVICE_ICONS_SCHEMA = cv.schema_with_slug_keys( - vol.Schema( + probatio.Schema( { - vol.Optional("service"): icon_value_validator, - vol.Optional("sections"): cv.schema_with_slug_keys( + probatio.Optional("service"): icon_value_validator, + probatio.Optional("sections"): cv.schema_with_slug_keys( icon_value_validator, slug_validator=translation_key_validator ), } @@ -104,12 +106,12 @@ def ensure_range_is_sorted(value: dict) -> dict: CUSTOM_INTEGRATION_SERVICE_ICONS_SCHEMA = cv.schema_with_slug_keys( - vol.All( + probatio.All( convert_shorthand_service_icon, - vol.Schema( + probatio.Schema( { - vol.Optional("service"): icon_value_validator, - vol.Optional("sections"): cv.schema_with_slug_keys( + probatio.Optional("service"): icon_value_validator, + probatio.Optional("sections"): cv.schema_with_slug_keys( icon_value_validator, slug_validator=translation_key_validator ), } @@ -120,9 +122,9 @@ def ensure_range_is_sorted(value: dict) -> dict: CONDITION_ICONS_SCHEMA = cv.schema_with_slug_keys( - vol.Schema( + probatio.Schema( { - vol.Optional("condition"): icon_value_validator, + probatio.Optional("condition"): icon_value_validator, } ), slug_validator=cv.underscore_slug, @@ -130,9 +132,9 @@ def ensure_range_is_sorted(value: dict) -> dict: TRIGGER_ICONS_SCHEMA = cv.schema_with_slug_keys( - vol.Schema( + probatio.Schema( { - vol.Optional("trigger"): icon_value_validator, + probatio.Optional("trigger"): icon_value_validator, } ), slug_validator=cv.underscore_slug, @@ -141,7 +143,7 @@ def ensure_range_is_sorted(value: dict) -> dict: def icon_schema( core_integration: bool, integration_type: IntegrationType, no_entity_platform: bool -) -> vol.Schema: +) -> probatio.Schema: """Create an icon schema.""" state_validator = cv.schema_with_slug_keys( @@ -154,17 +156,17 @@ def icon_schema( slug_validator=range_key_validator, ) - def icon_schema_slug(marker: type[vol.Marker]) -> dict[vol.Marker, Any]: + def icon_schema_slug(marker: type[probatio.Marker]) -> dict[probatio.Marker, Any]: return { marker("default"): icon_value_validator, - vol.Optional("state"): state_validator, - vol.Optional("range"): range_validator, - vol.Optional("state_attributes"): vol.All( + probatio.Optional("state"): state_validator, + probatio.Optional("range"): range_validator, + probatio.Optional("state_attributes"): probatio.All( cv.schema_with_slug_keys( { marker("default"): icon_value_validator, - vol.Optional("state"): state_validator, - vol.Optional("range"): range_validator, + probatio.Optional("state"): state_validator, + probatio.Optional("range"): range_validator, }, slug_validator=translation_key_validator, ), @@ -173,18 +175,18 @@ def icon_schema_slug(marker: type[vol.Marker]) -> dict[vol.Marker, Any]: ), } - schema = vol.Schema( + schema = probatio.Schema( { - vol.Optional("conditions"): CONDITION_ICONS_SCHEMA, - vol.Optional("config"): DATA_ENTRY_ICONS_SCHEMA, - vol.Optional("issues"): vol.Schema( + probatio.Optional("conditions"): CONDITION_ICONS_SCHEMA, + probatio.Optional("config"): DATA_ENTRY_ICONS_SCHEMA, + probatio.Optional("issues"): probatio.Schema( {str: {"fix_flow": DATA_ENTRY_ICONS_SCHEMA}} ), - vol.Optional("options"): DATA_ENTRY_ICONS_SCHEMA, - vol.Optional("services"): CORE_SERVICE_ICONS_SCHEMA + probatio.Optional("options"): DATA_ENTRY_ICONS_SCHEMA, + probatio.Optional("services"): CORE_SERVICE_ICONS_SCHEMA if core_integration else CUSTOM_INTEGRATION_SERVICE_ICONS_SCHEMA, - vol.Optional("triggers"): TRIGGER_ICONS_SCHEMA, + probatio.Optional("triggers"): TRIGGER_ICONS_SCHEMA, } ) @@ -194,15 +196,15 @@ def icon_schema_slug(marker: type[vol.Marker]) -> dict[vol.Marker, Any]: IntegrationType.SYSTEM, ): if integration_type != IntegrationType.ENTITY or no_entity_platform: - field = vol.Optional("entity_component") + field = probatio.Optional("entity_component") else: - field = vol.Required("entity_component") + field = probatio.Required("entity_component") schema = schema.extend( { - field: vol.All( + field: probatio.All( cv.schema_with_slug_keys( - icon_schema_slug(vol.Required), - slug_validator=vol.Any("_", cv.slug), + icon_schema_slug(probatio.Required), + slug_validator=probatio.Any("_", cv.slug), ), require_default_icon_validator, ensure_not_same_as_default, @@ -213,10 +215,10 @@ def icon_schema_slug(marker: type[vol.Marker]) -> dict[vol.Marker, Any]: if integration_type not in (IntegrationType.ENTITY, IntegrationType.SYSTEM): schema = schema.extend( { - vol.Optional("entity"): vol.All( + probatio.Optional("entity"): probatio.All( cv.schema_with_slug_keys( cv.schema_with_slug_keys( - icon_schema_slug(vol.Optional), + icon_schema_slug(probatio.Optional), slug_validator=translation_key_validator, ), slug_validator=cv.slug, @@ -250,7 +252,7 @@ def validate_icon_file(config: Config, integration: Integration) -> None: try: schema(icons) - except vol.Invalid as err: + except probatio.Invalid as err: integration.add_error("icons", f"Invalid {name}: {humanize_error(icons, err)}") diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index b4942018234f71..65239ee2c6b48b 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -12,8 +12,8 @@ AwesomeVersionException, AwesomeVersionStrategy, ) -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error from homeassistant.const import Platform from homeassistant.helpers import config_validation as cv @@ -141,7 +141,7 @@ def core_documentation_url(value: str) -> str: if value in DOCUMENTATION_URL_EXCEPTIONS: return value if not value.startswith(_CORE_DOCUMENTATION_BASE): - raise vol.Invalid( + raise probatio.Invalid( f"Documentation URL does not begin with {_CORE_DOCUMENTATION_BASE}" ) @@ -152,9 +152,9 @@ def custom_documentation_url(value: str) -> str: """Validate that a custom integration documentation url is correct.""" parsed_url = urlparse(value) if parsed_url.scheme != DOCUMENTATION_URL_SCHEMA: - raise vol.Invalid("Documentation url is not prefixed with https") + raise probatio.Invalid("Documentation url is not prefixed with https") if value.startswith(_CORE_DOCUMENTATION_BASE): - raise vol.Invalid( + raise probatio.Invalid( "Documentation URL should point to the custom integration documentation" ) @@ -164,7 +164,7 @@ def custom_documentation_url(value: str) -> str: def verify_lowercase(value: str) -> str: """Verify a value is lowercase.""" if value.lower() != value: - raise vol.Invalid("Value needs to be lowercase") + raise probatio.Invalid("Value needs to be lowercase") return value @@ -172,7 +172,7 @@ def verify_lowercase(value: str) -> str: def verify_uppercase(value: str) -> str: """Verify a value is uppercase.""" if value.upper() != value: - raise vol.Invalid("Value needs to be uppercase") + raise probatio.Invalid("Value needs to be uppercase") return value @@ -191,45 +191,49 @@ def verify_version(value: str) -> str: ], ) except AwesomeVersionException as err: - raise vol.Invalid(f"'{value}' is not a valid version.") from err + raise probatio.Invalid(f"'{value}' is not a valid version.") from err return value def verify_wildcard(value: str) -> str: """Verify the matcher contains a wildcard.""" if "*" not in value: - raise vol.Invalid(f"'{value}' needs to contain a wildcard matcher") + raise probatio.Invalid(f"'{value}' needs to contain a wildcard matcher") return value -INTEGRATION_MANIFEST_SCHEMA = vol.Schema( +INTEGRATION_MANIFEST_SCHEMA = probatio.Schema( { - vol.Required("domain"): str, - vol.Required("name"): str, - vol.Optional("integration_type", default="hub"): vol.In( + probatio.Required("domain"): str, + probatio.Required("name"): str, + probatio.Optional("integration_type", default="hub"): probatio.In( [t.value for t in IntegrationType if t != IntegrationType.VIRTUAL] ), - vol.Optional("config_flow"): bool, - vol.Optional("mqtt"): [str], - vol.Optional("zeroconf"): [ - vol.Any( + probatio.Optional("config_flow"): bool, + probatio.Optional("mqtt"): [str], + probatio.Optional("zeroconf"): [ + probatio.Any( str, - vol.All( + probatio.All( cv.deprecated("macaddress"), cv.deprecated("model"), cv.deprecated("manufacturer"), - vol.Schema( + probatio.Schema( { - vol.Required("type"): str, - vol.Optional("macaddress"): vol.All( + probatio.Required("type"): str, + probatio.Optional("macaddress"): probatio.All( str, verify_uppercase, verify_wildcard ), - vol.Optional("manufacturer"): vol.All( + probatio.Optional("manufacturer"): probatio.All( str, verify_lowercase ), - vol.Optional("model"): vol.All(str, verify_lowercase), - vol.Optional("name"): vol.All(str, verify_lowercase), - vol.Optional("properties"): vol.Schema( + probatio.Optional("model"): probatio.All( + str, verify_lowercase + ), + probatio.Optional("name"): probatio.All( + str, verify_lowercase + ), + probatio.Optional("properties"): probatio.Schema( {str: verify_lowercase} ), } @@ -237,62 +241,83 @@ def verify_wildcard(value: str) -> str: ), ) ], - vol.Optional("ssdp"): vol.Schema( - vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) + probatio.Optional("ssdp"): probatio.Schema( + probatio.All( + [ + probatio.All( + probatio.Schema({}, extra=probatio.ALLOW_EXTRA), + probatio.Length(min=1), + ) + ] + ) ), - vol.Optional("bluetooth"): [ - vol.Schema( + probatio.Optional("bluetooth"): [ + probatio.Schema( { - vol.Optional("connectable"): bool, - vol.Optional("service_uuid"): vol.All(str, verify_lowercase), - vol.Optional("service_data_uuid"): vol.All(str, verify_lowercase), - vol.Optional("local_name"): vol.All(str), - vol.Optional("manufacturer_id"): int, - vol.Optional("manufacturer_data_start"): [int], + probatio.Optional("connectable"): bool, + probatio.Optional("service_uuid"): probatio.All( + str, verify_lowercase + ), + probatio.Optional("service_data_uuid"): probatio.All( + str, verify_lowercase + ), + probatio.Optional("local_name"): probatio.All(str), + probatio.Optional("manufacturer_id"): int, + probatio.Optional("manufacturer_data_start"): [int], } ) ], - vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), - vol.Optional("dhcp"): [ - vol.Schema( + probatio.Optional("homekit"): probatio.Schema( + {probatio.Optional("models"): [str]} + ), + probatio.Optional("dhcp"): [ + probatio.Schema( { - vol.Optional("macaddress"): vol.All( + probatio.Optional("macaddress"): probatio.All( str, verify_uppercase, verify_wildcard ), - vol.Optional("hostname"): vol.All(str, verify_lowercase), - vol.Optional("registered_devices"): cv.boolean, + probatio.Optional("hostname"): probatio.All(str, verify_lowercase), + probatio.Optional("registered_devices"): cv.boolean, } ) ], - vol.Optional("usb"): [ - vol.Schema( + probatio.Optional("usb"): [ + probatio.Schema( { - vol.Optional("vid"): vol.All(str, verify_uppercase), - vol.Optional("pid"): vol.All(str, verify_uppercase), - vol.Optional("serial_number"): vol.All(str, verify_lowercase), - vol.Optional("manufacturer"): vol.All(str, verify_lowercase), - vol.Optional("description"): vol.All(str, verify_lowercase), - vol.Optional("known_devices"): [str], + probatio.Optional("vid"): probatio.All(str, verify_uppercase), + probatio.Optional("pid"): probatio.All(str, verify_uppercase), + probatio.Optional("serial_number"): probatio.All( + str, verify_lowercase + ), + probatio.Optional("manufacturer"): probatio.All( + str, verify_lowercase + ), + probatio.Optional("description"): probatio.All( + str, verify_lowercase + ), + probatio.Optional("known_devices"): [str], } ) ], - vol.Required("documentation"): vol.All(vol.Url(), core_documentation_url), - vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), - vol.Optional("requirements"): [str], - vol.Optional("dependencies"): [str], - vol.Optional("after_dependencies"): [str], - vol.Required("codeowners"): [str], - vol.Optional("loggers"): [str], - vol.Optional("disabled"): str, - vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES), - vol.Optional("single_config_entry"): bool, - vol.Optional("preview_features"): vol.Schema( + probatio.Required("documentation"): probatio.All( + probatio.Url(), core_documentation_url + ), + probatio.Optional("quality_scale"): probatio.In(SUPPORTED_QUALITY_SCALES), + probatio.Optional("requirements"): [str], + probatio.Optional("dependencies"): [str], + probatio.Optional("after_dependencies"): [str], + probatio.Required("codeowners"): [str], + probatio.Optional("loggers"): [str], + probatio.Optional("disabled"): str, + probatio.Optional("iot_class"): probatio.In(SUPPORTED_IOT_CLASSES), + probatio.Optional("single_config_entry"): bool, + probatio.Optional("preview_features"): probatio.Schema( { - cv.slug: vol.Schema( + cv.slug: probatio.Schema( { - vol.Optional("feedback_url"): vol.Url(), - vol.Optional("learn_more_url"): vol.Url(), - vol.Optional("report_issue_url"): vol.Url(), + probatio.Optional("feedback_url"): probatio.Url(), + probatio.Optional("learn_more_url"): probatio.Url(), + probatio.Optional("report_issue_url"): probatio.Url(), } ) } @@ -300,20 +325,20 @@ def verify_wildcard(value: str) -> str: } ) -VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema( +VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = probatio.Schema( { - vol.Required("domain"): str, - vol.Required("name"): str, - vol.Required("integration_type"): IntegrationType.VIRTUAL.value, - vol.Exclusive("iot_standards", "virtual_integration"): [ - vol.Any("homekit", "zigbee", "zwave") + probatio.Required("domain"): str, + probatio.Required("name"): str, + probatio.Required("integration_type"): IntegrationType.VIRTUAL.value, + probatio.Exclusive("iot_standards", "virtual_integration"): [ + probatio.Any("homekit", "zigbee", "zwave") ], - vol.Exclusive("supported_by", "virtual_integration"): str, + probatio.Exclusive("supported_by", "virtual_integration"): str, } ) -def manifest_schema(value: dict[str, Any]) -> vol.Schema: +def manifest_schema(value: dict[str, Any]) -> probatio.Schema: """Validate integration manifest.""" if value.get("integration_type") == IntegrationType.VIRTUAL: return VIRTUAL_INTEGRATION_MANIFEST_SCHEMA(value) @@ -322,10 +347,12 @@ def manifest_schema(value: dict[str, Any]) -> vol.Schema: CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend( { - vol.Required("documentation"): vol.All(vol.Url(), custom_documentation_url), - vol.Optional("version"): vol.All(str, verify_version), - vol.Optional("issue_tracker"): vol.Url(), - vol.Optional("import_executor"): bool, + probatio.Required("documentation"): probatio.All( + probatio.Url(), custom_documentation_url + ), + probatio.Optional("version"): probatio.All(str, verify_version), + probatio.Optional("issue_tracker"): probatio.Url(), + probatio.Optional("import_executor"): bool, } ) @@ -347,7 +374,7 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No manifest_schema(integration.manifest) else: CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) - except vol.Invalid as err: + except probatio.Invalid as err: integration.add_error( "manifest", f"Invalid manifest: {humanize_error(integration.manifest, err)}" ) diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index ece0ac1c0fd9a8..cce45425f70bc3 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -2,8 +2,8 @@ from dataclasses import dataclass -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error from homeassistant.const import Platform from homeassistant.exceptions import HomeAssistantError @@ -2061,22 +2061,22 @@ class Rule: "zone", ] -SCHEMA = vol.Schema( +SCHEMA = probatio.Schema( { - vol.Required("rules"): vol.Schema( + probatio.Required("rules"): probatio.Schema( { - vol.Required(rule.name): vol.Any( - vol.In(["todo", "done"]), - vol.Schema( + probatio.Required(rule.name): probatio.Any( + probatio.In(["todo", "done"]), + probatio.Schema( { - vol.Required("status"): vol.In(["todo", "done"]), - vol.Required("comment"): str, + probatio.Required("status"): probatio.In(["todo", "done"]), + probatio.Required("comment"): str, } ), - vol.Schema( + probatio.Schema( { - vol.Required("status"): "exempt", - vol.Required("comment"): str, + probatio.Required("status"): "exempt", + probatio.Required("comment"): str, } ), ) @@ -2183,7 +2183,7 @@ def validate_iqs_file(config: Config, integration: Integration) -> None: try: SCHEMA(data) - except vol.Invalid as err: + except probatio.Invalid as err: integration.add_error( "quality_scale", f"Invalid {name}: {humanize_error(data, err)}" ) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 1e2ae5c2e805f1..751e623ae5ba27 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -6,8 +6,8 @@ import re from typing import Any -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error from homeassistant.const import CONF_SELECTOR from homeassistant.exceptions import HomeAssistantError @@ -20,7 +20,7 @@ def exists(value: Any) -> Any: """Check if value exists.""" if value is None: - raise vol.Invalid("Value cannot be None") + raise probatio.Invalid("Value cannot be None") return value @@ -31,43 +31,45 @@ def unique_field_validator(fields: Any) -> Any: if value and "fields" in value: for key in value["fields"]: if key in all_fields: - raise vol.Invalid(f"Duplicate use of field {key} in service.") + raise probatio.Invalid(f"Duplicate use of field {key} in service.") all_fields.add(key) else: if key in all_fields: - raise vol.Invalid(f"Duplicate use of field {key} in service.") + raise probatio.Invalid(f"Duplicate use of field {key} in service.") all_fields.add(key) return fields CUSTOM_INTEGRATION_EXTRA_SCHEMA_DICT = { - vol.Optional("description"): str, - vol.Optional("name"): str, + probatio.Optional("description"): str, + probatio.Optional("name"): str, } CORE_INTEGRATION_NOT_TARGETED_FIELD_SCHEMA_DICT = { - vol.Optional("example"): exists, - vol.Optional("default"): exists, - vol.Optional("required"): bool, - vol.Optional("advanced"): bool, - vol.Optional(CONF_SELECTOR): selector.validate_selector, + probatio.Optional("example"): exists, + probatio.Optional("default"): exists, + probatio.Optional("required"): bool, + probatio.Optional("advanced"): bool, + probatio.Optional(CONF_SELECTOR): selector.validate_selector, } FIELD_FILTER_SCHEMA_DICT = { - vol.Optional("filter"): { - vol.Exclusive("attribute", "field_filter"): { - vol.Required(str): [vol.All(str, service.validate_attribute_option)], + probatio.Optional("filter"): { + probatio.Exclusive("attribute", "field_filter"): { + probatio.Required(str): [ + probatio.All(str, service.validate_attribute_option) + ], }, - vol.Exclusive("supported_features", "field_filter"): [ - vol.All(str, service.validate_supported_feature) + probatio.Exclusive("supported_features", "field_filter"): [ + probatio.All(str, service.validate_supported_feature) ], } } -def _field_schema(targeted: bool, custom: bool) -> vol.Schema: +def _field_schema(targeted: bool, custom: bool) -> probatio.Schema: """Return the field schema.""" schema_dict = CORE_INTEGRATION_NOT_TARGETED_FIELD_SCHEMA_DICT.copy() @@ -80,14 +82,14 @@ def _field_schema(targeted: bool, custom: bool) -> vol.Schema: if custom: schema_dict |= CUSTOM_INTEGRATION_EXTRA_SCHEMA_DICT - return vol.Schema(schema_dict) + return probatio.Schema(schema_dict) -def _section_schema(targeted: bool, custom: bool) -> vol.Schema: +def _section_schema(targeted: bool, custom: bool) -> probatio.Schema: """Return the section schema.""" schema_dict = { - vol.Optional("collapsed"): bool, - vol.Required("fields"): vol.Schema( + probatio.Optional("collapsed"): bool, + probatio.Required("fields"): probatio.Schema( { str: _field_schema(targeted, custom), } @@ -97,16 +99,16 @@ def _section_schema(targeted: bool, custom: bool) -> vol.Schema: if custom: schema_dict |= CUSTOM_INTEGRATION_EXTRA_SCHEMA_DICT - return vol.Schema(schema_dict) + return probatio.Schema(schema_dict) -def _service_schema(targeted: bool, custom: bool) -> vol.Schema: +def _service_schema(targeted: bool, custom: bool) -> probatio.Schema: """Return the service schema.""" schema_dict = { - vol.Optional("fields"): vol.All( - vol.Schema( + probatio.Optional("fields"): probatio.All( + probatio.Schema( { - str: vol.Any( + str: probatio.Any( _field_schema(targeted, custom), _section_schema(targeted, custom), ), @@ -119,14 +121,14 @@ def _service_schema(targeted: bool, custom: bool) -> vol.Schema: def raise_on_target_device_filter(value: dict[str, Any]) -> dict[str, Any]: """Raise error if target has a device filter.""" if "device" in value: - raise vol.Invalid( + raise probatio.Invalid( "Services do not support device filters on target, use a device " "selector instead" ) return value if targeted: - schema_dict[vol.Required("target")] = vol.All( + schema_dict[probatio.Required("target")] = probatio.All( selector.TargetSelector.CONFIG_SCHEMA, raise_on_target_device_filter, ) @@ -134,30 +136,30 @@ def raise_on_target_device_filter(value: dict[str, Any]) -> dict[str, Any]: if custom: schema_dict |= CUSTOM_INTEGRATION_EXTRA_SCHEMA_DICT - return vol.Schema(schema_dict) + return probatio.Schema(schema_dict) -CORE_INTEGRATION_SERVICE_SCHEMA = vol.Any( +CORE_INTEGRATION_SERVICE_SCHEMA = probatio.Any( _service_schema(targeted=True, custom=False), _service_schema(targeted=False, custom=False), None, ) -CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any( +CUSTOM_INTEGRATION_SERVICE_SCHEMA = probatio.Any( _service_schema(targeted=True, custom=True), _service_schema(targeted=False, custom=True), None, ) -CORE_INTEGRATION_SERVICES_SCHEMA = vol.Schema( +CORE_INTEGRATION_SERVICES_SCHEMA = probatio.Schema( { - vol.Remove(vol.All(str, service.starts_with_dot)): object, + probatio.Remove(probatio.All(str, service.starts_with_dot)): object, cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA, } ) -CUSTOM_INTEGRATION_SERVICES_SCHEMA = vol.Schema( +CUSTOM_INTEGRATION_SERVICES_SCHEMA = probatio.Schema( {cv.slug: CUSTOM_INTEGRATION_SERVICE_SCHEMA} ) @@ -238,7 +240,7 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa services = CORE_INTEGRATION_SERVICES_SCHEMA(data) else: services = CUSTOM_INTEGRATION_SERVICES_SCHEMA(data) - except vol.Invalid as err: + except probatio.Invalid as err: integration.add_error( "services", f"Invalid services.yaml: {humanize_error(data, err)}" ) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index ba358a0c9f5d5a..19e9f06bd0debc 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -6,8 +6,8 @@ import string from typing import Any -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import FRONTEND_HANDLED_ISSUES @@ -115,7 +115,7 @@ def removed_title_validator( ) -> Any: """Mark removed title.""" if not config.specific_integrations: - raise vol.Invalid(REMOVED_TITLE_MSG) + raise probatio.Invalid(REMOVED_TITLE_MSG) # Don't mark it as an error yet for custom components to allow backwards compat. integration.add_warning("translations", REMOVED_TITLE_MSG) @@ -125,7 +125,7 @@ def removed_title_validator( def translation_key_validator(value: str) -> str: """Validate value is valid translation key.""" if RE_TRANSLATION_KEY.match(value) is None: - raise vol.Invalid( + raise probatio.Invalid( f"Invalid translation key '{value}', need to be [a-z0-9-_]+ and" " cannot start or end with a hyphen or underscore." ) @@ -148,11 +148,13 @@ def validate_translation_value( string_value = string_no_single_quoted_placeholders(string_value) string_value = validate_placeholders(string_value, allow_placeholders) if RE_COMBINED_REFERENCE.search(string_value): - raise vol.Invalid("the string should not contain combined translations") + raise probatio.Invalid("the string should not contain combined translations") if string_value != string_value.strip(): - raise vol.Invalid("the string should not contain leading or trailing spaces") + raise probatio.Invalid( + "the string should not contain leading or trailing spaces" + ) if not allow_urls and RE_URL.search(string_value): - raise vol.Invalid( + raise probatio.Invalid( "the string should not contain URLs, " "please use description placeholders instead" ) @@ -178,7 +180,7 @@ def _validator(value: Any) -> str: def string_no_single_quoted_placeholders(value: str) -> str: """Validate that the value does not contain placeholders inside single quotes.""" if RE_PLACEHOLDER_IN_SINGLE_QUOTES.search(value): - raise vol.Invalid( + raise probatio.Invalid( "the string should not contain placeholders inside single quotes" ) return value @@ -191,9 +193,9 @@ def validate_placeholders(value: str, allow_placeholders: bool) -> str: for _, field_name, _, _ in formatter.parse(value): if field_name: # skip literal text segments if not allow_placeholders: - raise vol.Invalid("placeholders are not supported in this value") + raise probatio.Invalid("placeholders are not supported in this value") if not field_name.isidentifier(): - raise vol.Invalid( + raise probatio.Invalid( "placeholders must be valid identifiers ([a-zA-Z_][a-zA-Z0-9_]*)" ) return value @@ -207,53 +209,55 @@ def gen_data_entry_schema( require_step_title: bool, mandatory_description: str | None = None, subentry_flow: bool = False, -) -> vol.All: +) -> probatio.All: """Generate a data entry schema.""" - step_title_class = vol.Required if require_step_title else vol.Optional + step_title_class = probatio.Required if require_step_title else probatio.Optional schema = { - vol.Optional("flow_title"): translation_value_validator, - vol.Required("step"): { + probatio.Optional("flow_title"): translation_value_validator, + probatio.Required("step"): { str: { step_title_class("title"): translation_value_validator, - vol.Optional("description"): translation_value_validator, - vol.Optional("data"): {str: translation_value_validator}, - vol.Optional("data_description"): {str: translation_value_validator}, - vol.Optional("menu_options"): {str: translation_value_validator}, - vol.Optional("menu_option_descriptions"): { + probatio.Optional("description"): translation_value_validator, + probatio.Optional("data"): {str: translation_value_validator}, + probatio.Optional("data_description"): { + str: translation_value_validator + }, + probatio.Optional("menu_options"): {str: translation_value_validator}, + probatio.Optional("menu_option_descriptions"): { str: translation_value_validator }, - vol.Optional("submit"): translation_value_validator, - vol.Optional("sections"): { + probatio.Optional("submit"): translation_value_validator, + probatio.Optional("sections"): { str: { - vol.Optional("data"): {str: translation_value_validator}, - vol.Optional("data_description"): { + probatio.Optional("data"): {str: translation_value_validator}, + probatio.Optional("data_description"): { str: translation_value_validator }, - vol.Optional("description"): translation_value_validator, - vol.Optional("name"): translation_value_validator, + probatio.Optional("description"): translation_value_validator, + probatio.Optional("name"): translation_value_validator, }, }, } }, - vol.Optional("error"): {str: translation_value_validator}, - vol.Optional("abort"): {str: translation_value_validator}, - vol.Optional("progress"): {str: translation_value_validator}, - vol.Optional("create_entry"): {str: translation_value_validator}, + probatio.Optional("error"): {str: translation_value_validator}, + probatio.Optional("abort"): {str: translation_value_validator}, + probatio.Optional("progress"): {str: translation_value_validator}, + probatio.Optional("create_entry"): {str: translation_value_validator}, } if subentry_flow: - schema[vol.Required("entry_type")] = translation_value_validator - schema[vol.Required("initiate_flow")] = { - vol.Required("user"): translation_value_validator, + schema[probatio.Required("entry_type")] = translation_value_validator + schema[probatio.Required("initiate_flow")] = { + probatio.Required("user"): translation_value_validator, str: translation_value_validator, } else: - schema[vol.Optional("initiate_flow")] = { - vol.Required("user"): translation_value_validator, + schema[probatio.Optional("initiate_flow")] = { + probatio.Required("user"): translation_value_validator, } if flow_title == REQUIRED: - schema[vol.Required("title")] = translation_value_validator + schema[probatio.Required("title")] = translation_value_validator elif flow_title == REMOVED: - schema[vol.Optional("title", msg=REMOVED_TITLE_MSG)] = partial( + schema[probatio.Optional("title", msg=REMOVED_TITLE_MSG)] = partial( removed_title_validator, config, integration ) @@ -265,11 +269,11 @@ def data_description_validator(value: dict[str, Any]) -> dict[str, Any]: for key in step_info["data_description"]: if key not in step_info["data"]: - raise vol.Invalid(f"data_description key {key} is not in data") + raise probatio.Invalid(f"data_description key {key} is not in data") return value - validators = [vol.Schema(schema), data_description_validator] + validators = [probatio.Schema(schema), data_description_validator] if mandatory_description is not None: @@ -277,10 +281,12 @@ def validate_description_set(value: dict[str, Any]) -> dict[str, Any]: """Validate description is set.""" steps = value["step"] if mandatory_description not in steps: - raise vol.Invalid(f"{mandatory_description} needs to be defined") + raise probatio.Invalid(f"{mandatory_description} needs to be defined") if "description" not in steps[mandatory_description]: - raise vol.Invalid(f"Step {mandatory_description} needs a description") + raise probatio.Invalid( + f"Step {mandatory_description} needs a description" + ) return value @@ -292,7 +298,7 @@ def name_validator(value: dict[str, Any]) -> dict[str, Any]: """Validate name.""" for step_id, info in value["step"].items(): if info.get("title") == integration.name: - raise vol.Invalid( + raise probatio.Invalid( f"Do not set title of step {step_id} if it's a brand name " "or add exception to ALLOW_NAME_TRANSLATION" ) @@ -301,18 +307,20 @@ def name_validator(value: dict[str, Any]) -> dict[str, Any]: validators.append(name_validator) - return vol.All(*validators) + return probatio.All(*validators) def gen_issues_schema(config: Config, integration: Integration) -> dict[str, Any]: """Generate the issues schema.""" - issue_schema = vol.All( + issue_schema = probatio.All( cv.has_at_least_one_key("description", "fix_flow"), - vol.Schema( + probatio.Schema( { - vol.Required("title"): translation_value_validator, - vol.Exclusive("description", "fixable"): translation_value_validator, - vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( + probatio.Required("title"): translation_value_validator, + probatio.Exclusive( + "description", "fixable" + ): translation_value_validator, + probatio.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( config=config, integration=integration, flow_title=UNDEFINED, @@ -322,31 +330,31 @@ def gen_issues_schema(config: Config, integration: Integration) -> dict[str, Any ), ) - frontend_issue_schema = vol.Schema( - {vol.Required("title"): translation_value_validator} + frontend_issue_schema = probatio.Schema( + {probatio.Required("title"): translation_value_validator} ) schema: dict[str, Any] = {} for key in FRONTEND_HANDLED_ISSUES.get(integration.domain, ()): - schema[vol.Optional(key)] = frontend_issue_schema + schema[probatio.Optional(key)] = frontend_issue_schema schema[str] = issue_schema return schema _EXCEPTIONS_SCHEMA = { - vol.Optional("exceptions"): cv.schema_with_slug_keys( - {vol.Optional("message"): translation_value_validator}, + probatio.Optional("exceptions"): cv.schema_with_slug_keys( + {probatio.Optional("message"): translation_value_validator}, slug_validator=cv.slug, ), } -def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: +def gen_strings_schema(config: Config, integration: Integration) -> probatio.Schema: """Generate a strings schema.""" - return vol.Schema( + return probatio.Schema( { - vol.Optional("title"): translation_value_validator, - vol.Optional("config"): gen_data_entry_schema( + probatio.Optional("title"): translation_value_validator, + probatio.Optional("config"): gen_data_entry_schema( config=config, integration=integration, flow_title=REMOVED, @@ -357,7 +365,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: else None ), ), - vol.Optional("config_subentries"): cv.schema_with_slug_keys( + probatio.Optional("config_subentries"): cv.schema_with_slug_keys( gen_data_entry_schema( config=config, integration=integration, @@ -365,41 +373,47 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: require_step_title=False, subentry_flow=True, ), - slug_validator=vol.Any("_", cv.slug), + slug_validator=probatio.Any("_", cv.slug), ), - vol.Optional("options"): gen_data_entry_schema( + probatio.Optional("options"): gen_data_entry_schema( config=config, integration=integration, flow_title=UNDEFINED, require_step_title=False, ), - vol.Optional("preview_features"): cv.schema_with_slug_keys( + probatio.Optional("preview_features"): cv.schema_with_slug_keys( { - vol.Required("name"): translation_value_validator, - vol.Required("description"): custom_translation_value_validator( + probatio.Required("name"): translation_value_validator, + probatio.Required( + "description" + ): custom_translation_value_validator( allow_placeholders=False, allow_urls=True, ), - vol.Optional("enable_confirmation"): translation_value_validator, - vol.Optional("disable_confirmation"): translation_value_validator, + probatio.Optional( + "enable_confirmation" + ): translation_value_validator, + probatio.Optional( + "disable_confirmation" + ): translation_value_validator, }, slug_validator=translation_key_validator, ), - vol.Optional("selector"): cv.schema_with_slug_keys( + probatio.Optional("selector"): cv.schema_with_slug_keys( { - vol.Optional("choices"): cv.schema_with_slug_keys( + probatio.Optional("choices"): cv.schema_with_slug_keys( translation_value_validator, slug_validator=translation_key_validator, ), - vol.Optional("options"): cv.schema_with_slug_keys( + probatio.Optional("options"): cv.schema_with_slug_keys( translation_value_validator, slug_validator=translation_key_validator, ), - vol.Optional("unit_of_measurement"): cv.schema_with_slug_keys( + probatio.Optional("unit_of_measurement"): cv.schema_with_slug_keys( translation_value_validator, slug_validator=translation_key_validator, ), - vol.Optional("fields"): vol.Any( + probatio.Optional("fields"): probatio.Any( # Old format: # "key": "translation" cv.schema_with_slug_keys(str), @@ -410,8 +424,8 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: # } cv.schema_with_slug_keys( { - vol.Required("name"): str, - vol.Optional( + probatio.Required("name"): str, + probatio.Optional( "description" ): translation_value_validator, }, @@ -419,45 +433,47 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: ), ), }, - slug_validator=vol.Any("_", cv.slug), + slug_validator=probatio.Any("_", cv.slug), ), - vol.Optional("device_automation"): { - vol.Optional("action_type"): {str: translation_value_validator}, - vol.Optional("condition_type"): {str: translation_value_validator}, - vol.Optional("trigger_type"): {str: translation_value_validator}, - vol.Optional("trigger_subtype"): {str: translation_value_validator}, - vol.Optional("extra_fields"): {str: translation_value_validator}, - vol.Optional("extra_fields_descriptions"): { + probatio.Optional("device_automation"): { + probatio.Optional("action_type"): {str: translation_value_validator}, + probatio.Optional("condition_type"): {str: translation_value_validator}, + probatio.Optional("trigger_type"): {str: translation_value_validator}, + probatio.Optional("trigger_subtype"): { + str: translation_value_validator + }, + probatio.Optional("extra_fields"): {str: translation_value_validator}, + probatio.Optional("extra_fields_descriptions"): { str: translation_value_validator }, }, - vol.Optional("system_health"): { - vol.Optional("info"): cv.schema_with_slug_keys( + probatio.Optional("system_health"): { + probatio.Optional("info"): cv.schema_with_slug_keys( translation_value_validator, slug_validator=translation_key_validator, ), }, - vol.Optional("config_panel"): vol.Schema( - vol.Any( - {vol.Any(translation_key_validator, "_"): vol.Self}, + probatio.Optional("config_panel"): probatio.Schema( + probatio.Any( + {probatio.Any(translation_key_validator, "_"): probatio.Self}, translation_value_validator, ) ), - vol.Optional("application_credentials"): { - vol.Optional("description"): translation_value_validator, + probatio.Optional("application_credentials"): { + probatio.Optional("description"): translation_value_validator, }, - vol.Optional("issues"): gen_issues_schema(config, integration), - vol.Optional("entity_component"): cv.schema_with_slug_keys( + probatio.Optional("issues"): gen_issues_schema(config, integration), + probatio.Optional("entity_component"): cv.schema_with_slug_keys( { - vol.Optional("name"): str, - vol.Optional("state"): cv.schema_with_slug_keys( + probatio.Optional("name"): str, + probatio.Optional("state"): cv.schema_with_slug_keys( custom_translation_value_validator(allow_placeholders=False), slug_validator=translation_key_validator, ), - vol.Optional("state_attributes"): cv.schema_with_slug_keys( + probatio.Optional("state_attributes"): cv.schema_with_slug_keys( { - vol.Optional("name"): str, - vol.Optional("state"): cv.schema_with_slug_keys( + probatio.Optional("name"): str, + probatio.Optional("state"): cv.schema_with_slug_keys( custom_translation_value_validator( allow_placeholders=False ), @@ -467,32 +483,32 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: slug_validator=translation_key_validator, ), }, - slug_validator=vol.Any("_", cv.slug), + slug_validator=probatio.Any("_", cv.slug), ), - vol.Optional("device"): cv.schema_with_slug_keys( + probatio.Optional("device"): cv.schema_with_slug_keys( { - vol.Optional("name"): translation_value_validator, + probatio.Optional("name"): translation_value_validator, }, slug_validator=translation_key_validator, ), - vol.Optional("entity"): cv.schema_with_slug_keys( + probatio.Optional("entity"): cv.schema_with_slug_keys( cv.schema_with_slug_keys( { - vol.Optional("name"): translation_value_validator, - vol.Optional("state"): cv.schema_with_slug_keys( + probatio.Optional("name"): translation_value_validator, + probatio.Optional("state"): cv.schema_with_slug_keys( custom_translation_value_validator( allow_placeholders=False ), slug_validator=translation_key_validator, ), - vol.Optional("state_attributes"): cv.schema_with_slug_keys( + probatio.Optional("state_attributes"): cv.schema_with_slug_keys( { - vol.Optional( + probatio.Optional( "name" ): custom_translation_value_validator( allow_placeholders=False ), - vol.Optional("state"): cv.schema_with_slug_keys( + probatio.Optional("state"): cv.schema_with_slug_keys( custom_translation_value_validator( allow_placeholders=False ), @@ -501,7 +517,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: }, slug_validator=translation_key_validator, ), - vol.Optional( + probatio.Optional( "unit_of_measurement" ): custom_translation_value_validator(allow_placeholders=False), }, @@ -510,73 +526,83 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: slug_validator=cv.slug, ), **_EXCEPTIONS_SCHEMA, - vol.Optional("services"): cv.schema_with_slug_keys( + probatio.Optional("services"): cv.schema_with_slug_keys( { - vol.Required("name"): translation_value_validator, - vol.Required("description"): translation_value_validator, - vol.Optional("fields"): cv.schema_with_slug_keys( + probatio.Required("name"): translation_value_validator, + probatio.Required("description"): translation_value_validator, + probatio.Optional("fields"): cv.schema_with_slug_keys( { - vol.Required("name"): str, - vol.Optional("description"): translation_value_validator, - vol.Optional("example"): translation_value_validator, + probatio.Required("name"): str, + probatio.Optional( + "description" + ): translation_value_validator, + probatio.Optional("example"): translation_value_validator, }, slug_validator=translation_key_validator, ), - vol.Optional("sections"): cv.schema_with_slug_keys( + probatio.Optional("sections"): cv.schema_with_slug_keys( { - vol.Required("name"): str, - vol.Optional("description"): translation_value_validator, + probatio.Required("name"): str, + probatio.Optional( + "description" + ): translation_value_validator, }, slug_validator=translation_key_validator, ), }, slug_validator=translation_key_validator, ), - vol.Optional("conditions"): cv.schema_with_slug_keys( + probatio.Optional("conditions"): cv.schema_with_slug_keys( { - vol.Required("name"): translation_value_validator, - vol.Required("description"): translation_value_validator, - vol.Optional("fields"): cv.schema_with_slug_keys( + probatio.Required("name"): translation_value_validator, + probatio.Required("description"): translation_value_validator, + probatio.Optional("fields"): cv.schema_with_slug_keys( { - vol.Required("name"): str, - vol.Optional("description"): translation_value_validator, - vol.Optional("example"): translation_value_validator, + probatio.Required("name"): str, + probatio.Optional( + "description" + ): translation_value_validator, + probatio.Optional("example"): translation_value_validator, }, slug_validator=translation_key_validator, ), }, slug_validator=cv.underscore_slug, ), - vol.Optional("triggers"): cv.schema_with_slug_keys( + probatio.Optional("triggers"): cv.schema_with_slug_keys( { - vol.Required("name"): translation_value_validator, - vol.Required("description"): translation_value_validator, - vol.Optional("fields"): cv.schema_with_slug_keys( + probatio.Required("name"): translation_value_validator, + probatio.Required("description"): translation_value_validator, + probatio.Optional("fields"): cv.schema_with_slug_keys( { - vol.Required("name"): str, - vol.Optional("description"): translation_value_validator, - vol.Optional("example"): translation_value_validator, + probatio.Required("name"): str, + probatio.Optional( + "description" + ): translation_value_validator, + probatio.Optional("example"): translation_value_validator, }, slug_validator=translation_key_validator, ), }, slug_validator=cv.underscore_slug, ), - vol.Optional("conversation"): { - vol.Required("agent"): { - vol.Required("done"): translation_value_validator, + probatio.Optional("conversation"): { + probatio.Required("agent"): { + probatio.Required("done"): translation_value_validator, }, }, - vol.Optional("common"): vol.Schema({cv.slug: translation_value_validator}), + probatio.Optional("common"): probatio.Schema( + {cv.slug: translation_value_validator} + ), } ) -def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: +def gen_auth_schema(config: Config, integration: Integration) -> probatio.Schema: """Generate auth schema.""" - return vol.Schema( + return probatio.Schema( { - vol.Optional("mfa_setup"): { + probatio.Optional("mfa_setup"): { str: gen_data_entry_schema( config=config, integration=integration, @@ -584,7 +610,7 @@ def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: require_step_title=True, ) }, - vol.Optional("issues"): gen_issues_schema(config, integration), + probatio.Optional("issues"): gen_issues_schema(config, integration), **_EXCEPTIONS_SCHEMA, } ) @@ -592,10 +618,10 @@ def gen_auth_schema(config: Config, integration: Integration) -> vol.Schema: def gen_ha_hardware_schema(config: Config, integration: Integration): """Generate auth schema.""" - return vol.Schema( + return probatio.Schema( { str: { - vol.Optional("options"): gen_data_entry_schema( + probatio.Optional("options"): gen_data_entry_schema( config=config, integration=integration, flow_title=UNDEFINED, @@ -606,10 +632,10 @@ def gen_ha_hardware_schema(config: Config, integration: Integration): ) -ONBOARDING_SCHEMA = vol.Schema( +ONBOARDING_SCHEMA = probatio.Schema( { - vol.Required("area"): {str: translation_value_validator}, - vol.Required("dashboard"): {str: {"title": translation_value_validator}}, + probatio.Required("area"): {str: translation_value_validator}, + probatio.Required("dashboard"): {str: {"title": translation_value_validator}}, } ) @@ -655,7 +681,7 @@ def validate_translation_file( try: strings_schema(strings) - except vol.Invalid as err: + except probatio.Invalid as err: integration.add_error( "translations", f"Invalid {name}: {humanize_error(strings, err)}" ) diff --git a/script/hassfest/triggers.py b/script/hassfest/triggers.py index 9720a27c4c7d98..a627b02a73c44b 100644 --- a/script/hassfest/triggers.py +++ b/script/hassfest/triggers.py @@ -6,8 +6,8 @@ import re from typing import Any -import voluptuous as vol -from voluptuous.humanize import humanize_error +import probatio +from probatio.humanize import humanize_error from homeassistant.const import CONF_SELECTOR from homeassistant.exceptions import HomeAssistantError @@ -20,7 +20,7 @@ def exists(value: Any) -> Any: """Check if value exists.""" if value is None: - raise vol.Invalid("Value cannot be None") + raise probatio.Invalid("Value cannot be None") return value @@ -31,13 +31,15 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: # Validate context if present if "context" in field_schema: if CONF_SELECTOR not in field_schema: - raise vol.Invalid( + raise probatio.Invalid( f"Context defined without a selector in '{field_name}'" ) context = field_schema["context"] if not isinstance(context, dict): - raise vol.Invalid(f"Context must be a dictionary in '{field_name}'") + raise probatio.Invalid( + f"Context must be a dictionary in '{field_name}'" + ) # Determine which selector type is being used selector_config = field_schema[CONF_SELECTOR] @@ -50,7 +52,7 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: allowed = ( ", ".join(sorted(allowed_keys)) if allowed_keys else "none" ) - raise vol.Invalid( + raise probatio.Invalid( f"Invalid context key '{context_key}'" f" for selector type" f" '{selector_class.selector_type}'." @@ -59,7 +61,7 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: # Check if the referenced field exists in trigger schema or target if not isinstance(field_ref, str): - raise vol.Invalid( + raise probatio.Invalid( f"Context value for '{context_key}'" " must be a string field reference" ) @@ -76,7 +78,7 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: ): allowed_types = ", ".join(allowed_keys.get(context_key, set())) sel_type = field_selector_class.selector_type - raise vol.Invalid( + raise probatio.Invalid( f"The context '{context_key}' for" f" '{field_name}' references" f" '{field_ref}', but" @@ -91,7 +93,7 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: context_key, set() ): allowed_types = ", ".join(allowed_keys.get(context_key, set())) - raise vol.Invalid( + raise probatio.Invalid( f"The context '{context_key}' for" f" '{field_name}' references" f" 'target', but '{context_key}'" @@ -100,7 +102,7 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: ) if not field_exists: - raise vol.Invalid( + raise probatio.Invalid( f"Context reference '{field_ref}'" f" for key '{context_key}' does" " not exist in trigger schema" @@ -110,24 +112,24 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: return trigger_schema -FIELD_SCHEMA = vol.Schema( +FIELD_SCHEMA = probatio.Schema( { - vol.Optional("example"): exists, - vol.Optional("default"): exists, - vol.Optional("required"): bool, - vol.Optional(CONF_SELECTOR): selector.validate_selector, + probatio.Optional("example"): exists, + probatio.Optional("default"): exists, + probatio.Optional("required"): bool, + probatio.Optional(CONF_SELECTOR): selector.validate_selector, # key is context key, value is field name in schema # Validated in validate_field_schema - vol.Optional("context"): {str: str}, + probatio.Optional("context"): {str: str}, } ) -TRIGGER_SCHEMA = vol.Any( - vol.All( - vol.Schema( +TRIGGER_SCHEMA = probatio.Any( + probatio.All( + probatio.Schema( { - vol.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA, - vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), + probatio.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA, + probatio.Optional("fields"): probatio.Schema({str: FIELD_SCHEMA}), } ), validate_field_schema, @@ -135,9 +137,9 @@ def validate_field_schema(trigger_schema: dict[str, Any]) -> dict[str, Any]: None, ) -TRIGGERS_SCHEMA = vol.Schema( +TRIGGERS_SCHEMA = probatio.Schema( { - vol.Remove(vol.All(str, trigger.starts_with_dot)): object, + probatio.Remove(probatio.All(str, trigger.starts_with_dot)): object, cv.underscore_slug: TRIGGER_SCHEMA, } ) @@ -198,7 +200,7 @@ def validate_triggers(config: Config, integration: Integration) -> None: # noqa try: triggers = TRIGGERS_SCHEMA(data) - except vol.Invalid as err: + except probatio.Invalid as err: integration.add_error( "triggers", f"Invalid triggers.yaml: {humanize_error(data, err)}" ) diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index 226f375c2fbd70..d697c31c2f9d21 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -3,7 +3,7 @@ import logging from typing import Any -import voluptuous as vol +import probatio from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -14,11 +14,11 @@ _LOGGER = logging.getLogger(__name__) # TODO adjust the data schema to the data that you need -STEP_USER_DATA_SCHEMA = vol.Schema( +STEP_USER_DATA_SCHEMA = probatio.Schema( { - vol.Required(CONF_HOST): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + probatio.Required(CONF_HOST): str, + probatio.Required(CONF_USERNAME): str, + probatio.Required(CONF_PASSWORD): str, } ) diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index bdc7f92c41fa26..64dbefcf3c9302 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from typing import Any, cast -import voluptuous as vol +import probatio from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_ENTITY_ID @@ -16,17 +16,17 @@ from .const import DOMAIN -OPTIONS_SCHEMA = vol.Schema( +OPTIONS_SCHEMA = probatio.Schema( { - vol.Required(CONF_ENTITY_ID): selector.EntitySelector( + probatio.Required(CONF_ENTITY_ID): selector.EntitySelector( selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) ), } ) -CONFIG_SCHEMA = vol.Schema( +CONFIG_SCHEMA = probatio.Schema( { - vol.Required("name"): selector.TextSelector(), + probatio.Required("name"): selector.TextSelector(), } ).extend(OPTIONS_SCHEMA.schema) diff --git a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py index fbf705cfb26cda..42078f548661e4 100644 --- a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py @@ -52,7 +52,7 @@ async def test_config_flow( def get_suggested(schema, key): - """Get suggested value for key in voluptuous schema.""" + """Get suggested value for key in probatio schema.""" for k in schema: if k == key: if k.description is None or "suggested_value" not in k.description: diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py index 6f29094016854f..e86d316f276b35 100644 --- a/script/scaffold/templates/device_action/integration/device_action.py +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -1,6 +1,6 @@ """Provides device actions for NEW_NAME.""" -import voluptuous as vol +import probatio from homeassistant.const import ( ATTR_ENTITY_ID, @@ -21,8 +21,8 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { - vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), - vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + probatio.Required(CONF_TYPE): probatio.In(ACTION_TYPES), + probatio.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), } ) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 7e11b55e31a5cb..d3dda23cd17ea5 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,6 +1,6 @@ """Provide the device conditions for NEW_NAME.""" -import voluptuous as vol +import probatio from homeassistant.const import ( ATTR_ENTITY_ID, @@ -27,8 +27,8 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + probatio.Required(CONF_ENTITY_ID): cv.entity_id, + probatio.Required(CONF_TYPE): probatio.In(CONDITION_TYPES), } ) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index 6489e7ff7e69af..7d1338610d4d5c 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -2,7 +2,7 @@ from typing import Any -import voluptuous as vol +import probatio from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger @@ -27,8 +27,8 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + probatio.Required(CONF_ENTITY_ID): cv.entity_id, + probatio.Required(CONF_TYPE): probatio.In(TRIGGER_TYPES), } ) diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index 33dbbba25dd19c..009386183514ba 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -1,13 +1,15 @@ """The NEW_NAME integration.""" -import voluptuous as vol +import probatio from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = probatio.Schema( + {probatio.Optional(DOMAIN): {}}, extra=probatio.ALLOW_EXTRA +) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/tests/auth/permissions/test_entities.py b/tests/auth/permissions/test_entities.py index 3710f53050790f..dcbdb16a101761 100644 --- a/tests/auth/permissions/test_entities.py +++ b/tests/auth/permissions/test_entities.py @@ -1,7 +1,7 @@ """Tests for entity permissions.""" +import probatio import pytest -import voluptuous as vol from homeassistant.auth.permissions.entities import ( ENTITY_POLICY_SCHEMA, @@ -32,7 +32,7 @@ def test_entities_empty() -> None: def test_entities_false() -> None: """Test entity ID policy.""" policy = False - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ENTITY_POLICY_SCHEMA(policy) @@ -64,7 +64,7 @@ def test_entities_domains_domain_true() -> None: def test_entities_domains_domain_false() -> None: """Test entity ID policy.""" policy = {"domains": {"light": False}} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ENTITY_POLICY_SCHEMA(policy) @@ -79,7 +79,7 @@ def test_entities_entity_ids_true() -> None: def test_entities_entity_ids_false() -> None: """Test entity ID policy.""" policy = {"entity_ids": False} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ENTITY_POLICY_SCHEMA(policy) @@ -95,7 +95,7 @@ def test_entities_entity_ids_entity_id_true() -> None: def test_entities_entity_ids_entity_id_false() -> None: """Test entity ID policy.""" policy = {"entity_ids": {"light.kitchen": False}} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ENTITY_POLICY_SCHEMA(policy) diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 42a4a811b93fc4..14f31dfe2172b9 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -3,8 +3,8 @@ import asyncio from unittest.mock import Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, auth_store @@ -40,7 +40,7 @@ async def test_not_allow_set_id() -> None: """Test we are not allowed to set an ID in config.""" hass = Mock() hass.data = {} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await auth_provider_from_config( hass, None, {"type": "homeassistant", "id": "invalid"} ) diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 6b54f0fae48a82..3d6f333b27fa79 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -4,8 +4,8 @@ from unittest.mock import Mock, patch from hass_nabucasa import remote +import probatio import pytest -import voluptuous as vol from homeassistant import auth from homeassistant.auth import auth_store @@ -153,7 +153,7 @@ async def test_config_schema() -> None: } ) # Wrong user id format - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): tn_auth.CONFIG_SCHEMA( { "type": "trusted_networks", @@ -277,7 +277,7 @@ async def test_login_flow( schema = step["data_schema"] assert schema({"user": owner.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": "invalid-user"}) # login with valid user @@ -322,7 +322,7 @@ async def test_trusted_users_login( schema = step["data_schema"] # only owner listed assert schema({"user": owner.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": user.id}) # from trusted network, list users intersect trusted_users @@ -335,9 +335,9 @@ async def test_trusted_users_login( schema = step["data_schema"] # only user listed assert schema({"user": user.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": owner.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": sys_user.id}) # from trusted network, list users intersect trusted_users @@ -349,7 +349,7 @@ async def test_trusted_users_login( # both owner and user listed assert schema({"user": owner.id}) assert schema({"user": user.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": sys_user.id}) # from trusted network, list users intersect trusted_users @@ -361,11 +361,11 @@ async def test_trusted_users_login( schema = step["data_schema"] # no user listed - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": owner.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": user.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": sys_user.id}) @@ -409,7 +409,7 @@ async def test_trusted_group_login( schema = step["data_schema"] # only user listed assert schema({"user": user.id}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert schema({"user": owner.id}) # from trusted network, list users intersect trusted_users diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index eab90646580bd9..47ee426dcd13cd 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -7,8 +7,8 @@ from freezegun import freeze_time import jwt +import probatio import pytest -import voluptuous as vol from homeassistant import auth, data_entry_flow from homeassistant.auth import ( @@ -43,7 +43,7 @@ def mock_hass(hass: HomeAssistant) -> HomeAssistant: async def test_auth_manager_from_config_validates_config(mock_hass) -> None: """Test get auth providers.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): manager = await auth.auth_manager_from_config( mock_hass, [ @@ -84,7 +84,7 @@ async def test_auth_manager_from_config_validates_config(mock_hass) -> None: async def test_auth_manager_from_config_auth_modules(mock_hass) -> None: """Test get auth modules.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): manager = await auth.auth_manager_from_config( mock_hass, [ diff --git a/tests/common.py b/tests/common.py index fc658752ef60cb..9138c9c4056c16 100644 --- a/tests/common.py +++ b/tests/common.py @@ -30,9 +30,9 @@ from annotatedyaml import load_yaml_dict, loader as yaml_loader import attr from paho.mqtt.client import MQTTMessage +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader from homeassistant.auth import ( @@ -376,7 +376,7 @@ def async_mock_service( hass: HomeAssistant, domain: str, service: str, - schema: vol.Schema | None = None, + schema: probatio.Schema | None = None, response: ServiceResponse = None, supports_response: SupportsResponse | None = None, raise_exception: Exception | None = None, @@ -878,9 +878,9 @@ def __init__( dependencies: list[str] | None = None, setup: Callable[[HomeAssistant, ConfigType], bool] | None = None, requirements: list[str] | None = None, - config_schema: vol.Schema | None = None, - platform_schema: vol.Schema | None = None, - platform_schema_base: vol.Schema | None = None, + config_schema: probatio.Schema | None = None, + platform_schema: probatio.Schema | None = None, + platform_schema_base: probatio.Schema | None = None, async_setup: Callable[[HomeAssistant, ConfigType], Coroutine[Any, Any, bool]] | None = None, async_setup_entry: Callable[ @@ -973,7 +973,7 @@ def __init__( ] | None = None, dependencies: list[str] | None = None, - platform_schema: vol.Schema | None = None, + platform_schema: probatio.Schema | None = None, async_setup_platform: Callable[ [HomeAssistant, ConfigType, AddEntitiesCallback, DiscoveryInfoType | None], Coroutine[Any, Any, None], @@ -2008,8 +2008,8 @@ def get_quality_scale(integration: str) -> dict[str, QualityScaleStatus]: } -def get_schema_suggested_value(schema: vol.Schema, key: str) -> Any | None: - """Get suggested value for key in voluptuous schema.""" +def get_schema_suggested_value(schema: probatio.Schema, key: str) -> Any | None: + """Get suggested value for key in probatio schema.""" for schema_key in schema: if schema_key == key: if ( diff --git a/tests/components/ai_task/test_entity.py b/tests/components/ai_task/test_entity.py index 1dba7f06f4a097..21d5dfdff11d6e 100644 --- a/tests/components/ai_task/test_entity.py +++ b/tests/components/ai_task/test_entity.py @@ -1,7 +1,7 @@ """Tests for the AI Task entity model.""" +import probatio import pytest -import voluptuous as vol from homeassistant.components.ai_task import async_generate_data from homeassistant.const import STATE_UNKNOWN @@ -53,10 +53,10 @@ async def test_generate_structured_data( task_name="Test task", entity_id=TEST_ENTITY_ID, instructions="Please generate a profile for a new user", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("name"): selector.TextSelector(), - vol.Optional("age"): selector.NumberSelector( + probatio.Required("name"): selector.TextSelector(), + probatio.Optional("age"): selector.NumberSelector( config=selector.NumberSelectorConfig( min=0, max=120, @@ -76,4 +76,4 @@ async def test_generate_structured_data( task = mock_ai_task_entity.mock_generate_data_tasks[0] assert task.instructions == "Please generate a profile for a new user" assert task.structure - assert isinstance(task.structure, vol.Schema) + assert isinstance(task.structure, probatio.Schema) diff --git a/tests/components/ai_task/test_init.py b/tests/components/ai_task/test_init.py index a01d576a412300..d8ae3490e1d420 100644 --- a/tests/components/ai_task/test_init.py +++ b/tests/components/ai_task/test_init.py @@ -5,8 +5,8 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.ai_task import AITaskPreferences @@ -176,19 +176,19 @@ async def test_generate_data_service_structure_fields( task = mock_ai_task_entity.mock_generate_data_tasks[0] assert task.instructions == "Please generate a profile for a new user" assert task.structure - assert isinstance(task.structure, vol.Schema) + assert isinstance(task.structure, probatio.Schema) schema = list(task.structure.schema.items()) assert len(schema) == 2 name_key, name_value = schema[0] assert name_key == "name" - assert isinstance(name_key, vol.Required) + assert isinstance(name_key, probatio.Required) assert name_key.description == "First and last name of the user such as Alice Smith" assert isinstance(name_value, selector.TextSelector) age_key, age_value = schema[1] assert age_key == "age" - assert isinstance(age_key, vol.Optional) + assert isinstance(age_key, probatio.Optional) assert age_key.description == "Age of the user" assert isinstance(age_value, selector.NumberSelector) assert age_value.config["min"] == 0 @@ -207,7 +207,7 @@ async def test_generate_data_service_structure_fields( "selector": {"invalid-selector": {}}, }, }, - vol.Invalid, + probatio.Invalid, r"Unknown selector type invalid-selector.*", ), ( @@ -223,7 +223,7 @@ async def test_generate_data_service_structure_fields( }, }, }, - vol.Invalid, + probatio.Invalid, r"not a valid option.*", ), ( @@ -234,12 +234,12 @@ async def test_generate_data_service_structure_fields( ), }, }, - vol.Invalid, + probatio.Invalid, r"required key not provided.*selector.*", ), - (12345, vol.Invalid, r"xpected a mapping.*"), - ("name", vol.Invalid, r"xpected a mapping.*"), - (["name"], vol.Invalid, r"xpected a mapping.*"), + (12345, probatio.Invalid, r"xpected a mapping.*"), + ("name", probatio.Invalid, r"xpected a mapping.*"), + (["name"], probatio.Invalid, r"xpected a mapping.*"), ( { "name": { @@ -250,7 +250,7 @@ async def test_generate_data_service_structure_fields( "extra-fields": "Some extra fields", }, }, - vol.Invalid, + probatio.Invalid, r"not a valid option .*", ), ( @@ -262,7 +262,7 @@ async def test_generate_data_service_structure_fields( "selector": "invalid-schema", }, }, - vol.Invalid, + probatio.Invalid, r"xpected a dictionary.*", ), ], diff --git a/tests/components/airos/test_config_flow.py b/tests/components/airos/test_config_flow.py index 3dac32336ebe80..6b0c51dda53984 100644 --- a/tests/components/airos/test_config_flow.py +++ b/tests/components/airos/test_config_flow.py @@ -12,8 +12,8 @@ AirOSTLSCompatibilityError, ) from airos.helpers import DetectDeviceData +import probatio import pytest -import voluptuous as vol from homeassistant.components.airos.const import ( CONF_LEGACY_SSL, @@ -661,7 +661,9 @@ async def test_discover_flow_multiple_devices_found( f"{MOCK_DISC_DEV2[HOSTNAME]} ({MOCK_DISC_DEV2[IP_ADDRESS]})" ), } - actual_options = result["data_schema"].schema[vol.Required(MAC_ADDRESS)].container + actual_options = ( + result["data_schema"].schema[probatio.Required(MAC_ADDRESS)].container + ) assert actual_options == expected_options # Select one of the devices diff --git a/tests/components/amberelectric/test_services.py b/tests/components/amberelectric/test_services.py index 26a5503a39bb1f..e3a7ea5a7f33e6 100644 --- a/tests/components/amberelectric/test_services.py +++ b/tests/components/amberelectric/test_services.py @@ -2,8 +2,8 @@ import re +import probatio import pytest -import voluptuous as vol from homeassistant.components.amberelectric.const import DOMAIN from homeassistant.components.amberelectric.services import ATTR_CHANNEL_TYPE @@ -123,7 +123,7 @@ async def test_incorrect_channel_type( await setup_integration(hass, general_channel_config_entry) with pytest.raises( - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, match=re.escape( "value must be one of ['controlled_load', 'feed_in'," " 'general'] at 'channel_type'" diff --git a/tests/components/anthropic/test_ai_task.py b/tests/components/anthropic/test_ai_task.py index 59aa1f0e498707..10f7e8a82ce6b8 100644 --- a/tests/components/anthropic/test_ai_task.py +++ b/tests/components/anthropic/test_ai_task.py @@ -6,9 +6,9 @@ from anthropic.types import Message, TextBlock, Usage from freezegun import freeze_time +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import ai_task, media_source from homeassistant.components.anthropic.const import ( @@ -150,9 +150,9 @@ async def test_generate_structured_data_legacy( task_name="Test Task", entity_id="ai_task.claude_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -202,9 +202,9 @@ async def test_generate_structured_data_legacy_tools( task_name="Test Task", entity_id="ai_task.claude_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -262,9 +262,9 @@ async def test_generate_structured_data_legacy_extended_thinking( task_name="Test Task", entity_id="ai_task.claude_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -323,9 +323,9 @@ async def test_generate_structured_data_legacy_extra_text_block( task_name="Test Task", entity_id="ai_task.claude_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -374,9 +374,9 @@ async def test_generate_invalid_structured_data_legacy( task_name="Test Task", entity_id="ai_task.claude_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -406,9 +406,9 @@ async def test_generate_structured_data( task_name="Test Task", entity_id="ai_task.claude_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index bb600cd7f6bba8..d8afbe64dffa72 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -39,9 +39,9 @@ ) from freezegun import freeze_time from httpx import URL, Request, Response +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import conversation from homeassistant.components.anthropic.const import ( @@ -430,8 +430,8 @@ async def test_function_call( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) mock_tool.async_call.return_value = "Test response" @@ -510,8 +510,8 @@ async def test_function_exception( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) mock_tool.async_call.side_effect = HomeAssistantError("Test tool exception") @@ -953,8 +953,8 @@ async def test_extended_thinking_tool_call( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) mock_tool.async_call.return_value = "Test response" diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index e407f1b084fc54..734cad05fe3ed8 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -8,9 +8,9 @@ from aiohttp import ServerDisconnectedError, web from aiohttp.test_utils import TestClient +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant import const, core as ha from homeassistant.auth.models import Credentials @@ -1009,7 +1009,7 @@ def listener(service_call): test_value.append(1) hass.services.async_register( - "test_domain", "test_service", listener, schema=vol.Schema({"hello": str}) + "test_domain", "test_service", listener, schema=probatio.Schema({"hello": str}) ) resp = await mock_api_client.post( diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 4d231d1c292f53..06a6da97092c36 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -7,9 +7,9 @@ from freezegun import freeze_time from hassil.recognize import Intent, IntentData, RecognizeResult +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import ( assist_pipeline, @@ -1818,7 +1818,7 @@ async def stream_llm_response(): mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema({}) + mock_tool.parameters = probatio.Schema({}) mock_tool.async_call.return_value = "Test response" with ( diff --git a/tests/components/assist_satellite/test_entity.py b/tests/components/assist_satellite/test_entity.py index 07c0ffc2ab26fb..ec44f2c6b256a1 100644 --- a/tests/components/assist_satellite/test_entity.py +++ b/tests/components/assist_satellite/test_entity.py @@ -5,8 +5,8 @@ from dataclasses import asdict from unittest.mock import Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import stt from homeassistant.components.assist_pipeline import ( @@ -1021,7 +1021,7 @@ async def test_ask_question_invalid_sentences( sentence: str, ) -> None: """Test that invalid sentences raise an exception.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "ask_question", diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index e2857ba9219aca..c90fd0a5854b2a 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -6,8 +6,8 @@ from typing import Any from unittest.mock import ANY, Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import automation, input_boolean, script from homeassistant.components.automation import ( @@ -1940,7 +1940,7 @@ async def test_automation_with_error_in_script_2( False, ), ( - vol.Invalid("not valid"), + probatio.Invalid("not valid"), "Error while executing automation automation.hello: not valid", False, ), @@ -1950,7 +1950,7 @@ async def test_automation_with_error_in_script_2( True, ), ], - ids=["home_assistant_error", "voluptuous_invalid", "unexpected_exception"], + ids=["home_assistant_error", "probatio_invalid", "unexpected_exception"], ) async def test_automation_with_error_in_action_script( hass: HomeAssistant, @@ -1984,7 +1984,7 @@ async def test_automation_with_error_in_action_script( assert len(calls) == 0 assert expected_error in caplog.text - # A HomeAssistantError/voluptuous error is logged without a traceback, an + # A HomeAssistantError/probatio error is logged without a traceback, an # unexpected error is logged with a traceback. assert ("Traceback" in caplog.text) is expect_traceback @@ -2009,7 +2009,7 @@ async def test_automation_with_error_in_action_script( False, ), ( - vol.Invalid("not valid"), + probatio.Invalid("not valid"), "Error while checking conditions of automation automation.hello: not valid", False, ), @@ -2019,7 +2019,7 @@ async def test_automation_with_error_in_action_script( True, ), ], - ids=["home_assistant_error", "voluptuous_invalid", "unexpected_exception"], + ids=["home_assistant_error", "probatio_invalid", "unexpected_exception"], ) async def test_automation_with_error_in_condition( hass: HomeAssistant, @@ -2059,7 +2059,7 @@ async def test_automation_with_error_in_condition( # The action must not run when the condition check raises. assert len(calls) == 0 assert expected_error in caplog.text - # A HomeAssistantError/voluptuous error is logged without a traceback, an + # A HomeAssistantError/probatio error is logged without a traceback, an # unexpected error is logged with a traceback. assert ("Traceback" in caplog.text) is expect_traceback diff --git a/tests/components/azure_data_explorer/test_config_flow.py b/tests/components/azure_data_explorer/test_config_flow.py index 267eacea568551..4a7e6f833b3f73 100644 --- a/tests/components/azure_data_explorer/test_config_flow.py +++ b/tests/components/azure_data_explorer/test_config_flow.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock from azure.kusto.data.exceptions import KustoAuthenticationError, KustoServiceError +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components.azure_data_explorer.const import ( @@ -89,12 +89,12 @@ async def test_config_flow_errors( assert result2["errors"] == {"base": expected} schema = result2["data_schema"] - assert isinstance(schema, vol.Schema) + assert isinstance(schema, probatio.Schema) suggested_values = { key.schema: key.description.get("suggested_value") for key in schema.schema - if isinstance(key, vol.Marker) + if isinstance(key, probatio.Marker) and key.description and "suggested_value" in key.description } diff --git a/tests/components/bang_olufsen/test_media_player.py b/tests/components/bang_olufsen/test_media_player.py index 25d3e805ef3d31..0099222ee605f3 100644 --- a/tests/components/bang_olufsen/test_media_player.py +++ b/tests/components/bang_olufsen/test_media_player.py @@ -15,10 +15,10 @@ SourceArray, WebsocketNotificationTag, ) +from probatio import Invalid, MultipleInvalid import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props -from voluptuous import Invalid, MultipleInvalid from homeassistant.components.bang_olufsen.const import ( BEO_REPEAT_FROM_HA, diff --git a/tests/components/bayesian/test_config_flow.py b/tests/components/bayesian/test_config_flow.py index a6970f1bca103f..05d0c46e0e68ab 100644 --- a/tests/components/bayesian/test_config_flow.py +++ b/tests/components/bayesian/test_config_flow.py @@ -3,8 +3,8 @@ from types import MappingProxyType from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.bayesian.config_flow import ( @@ -1023,7 +1023,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None: assert result0["type"] is FlowResultType.FORM # priors should never be Zero, because then the sensor can never return 'on' - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: result = await hass.config_entries.flow.async_configure( result0["flow_id"], { @@ -1036,7 +1036,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None: assert excinfo.value.error_message == "extreme_prior_error" # priors should never be 100% because then the sensor can never be 'off' - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: result = await hass.config_entries.flow.async_configure( result0["flow_id"], { @@ -1049,7 +1049,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None: assert excinfo.value.error_message == "extreme_prior_error" # Threshold should never be 100% because then the sensor can never be 'on' - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: result = await hass.config_entries.flow.async_configure( result0["flow_id"], { @@ -1062,7 +1062,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None: assert excinfo.value.error_message == "extreme_threshold_error" # Threshold should never be 0 because then the sensor can never be 'off' - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: result = await hass.config_entries.flow.async_configure( result0["flow_id"], { @@ -1099,7 +1099,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM # Observations with a probability of 0 will create certainties - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: result = await hass.config_entries.subentries.async_configure( result["flow_id"], { @@ -1114,7 +1114,7 @@ async def test_invalid_configs(hass: HomeAssistant) -> None: assert excinfo.value.error_message == "extreme_prob_given_error" # Observations with a probability of 1 will create certainties - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: result = await hass.config_entries.subentries.async_configure( result["flow_id"], { diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index 5de41a1fb1e772..50e860292c6dd2 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -3,8 +3,8 @@ from collections import defaultdict from unittest import mock +import probatio import pytest -import voluptuous as vol from homeassistant.components.blackbird.const import DOMAIN, SERVICE_SETALLZONES from homeassistant.components.blackbird.media_player import ( @@ -166,7 +166,7 @@ def test_invalid_schemas() -> None: }, ) for value in schemas: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): PLATFORM_SCHEMA(value) diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index 4225177cfd5e73..0948aa85da7a72 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -9,8 +9,8 @@ RequestLimitReached, WebsocketError, ) +from probatio import MultipleInvalid import pytest -from voluptuous import MultipleInvalid from homeassistant.components.blue_current.const import ( CHARGING_CARD_ID, diff --git a/tests/components/blueprint/test_schemas.py b/tests/components/blueprint/test_schemas.py index 70d599c9d01ec6..a6998427c92258 100644 --- a/tests/components/blueprint/test_schemas.py +++ b/tests/components/blueprint/test_schemas.py @@ -2,8 +2,8 @@ import logging +import probatio import pytest -import voluptuous as vol from homeassistant.components.blueprint import schemas @@ -76,7 +76,7 @@ def test_blueprint_schema(blueprint) -> None: """Test different schemas.""" try: schemas.BLUEPRINT_SCHEMA(blueprint) - except vol.Invalid: + except probatio.Invalid: _LOGGER.exception("%s", blueprint) pytest.fail("Expected schema to be valid") @@ -144,7 +144,7 @@ def test_blueprint_schema(blueprint) -> None: ) def test_blueprint_schema_invalid(blueprint) -> None: """Test different schemas.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schemas.BLUEPRINT_SCHEMA(blueprint) diff --git a/tests/components/bosch_alarm/test_services.py b/tests/components/bosch_alarm/test_services.py index 377ddb4ba45930..a223afcc8df297 100644 --- a/tests/components/bosch_alarm/test_services.py +++ b/tests/components/bosch_alarm/test_services.py @@ -5,8 +5,8 @@ import datetime as dt from unittest.mock import AsyncMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.bosch_alarm.const import ( ATTR_DATETIME, @@ -82,7 +82,7 @@ async def test_set_date_time_service_fails_bad_params( """Test that the service calls fail if done with incorrect params.""" await setup_integration(hass, mock_config_entry) with pytest.raises( - vol.MultipleInvalid, + probatio.MultipleInvalid, match=r"Invalid datetime specified: at 'datetime'", ): await hass.services.async_call( @@ -105,7 +105,7 @@ async def test_set_date_time_service_fails_bad_year_before( """Test that the service calls fail if the panel fails the service call.""" await setup_integration(hass, mock_config_entry) with pytest.raises( - vol.MultipleInvalid, + probatio.MultipleInvalid, match=r"datetime must be before 2038 at 'datetime'", ): await hass.services.async_call( @@ -129,7 +129,7 @@ async def test_set_date_time_service_fails_bad_year_after( await setup_integration(hass, mock_config_entry) mock_panel.set_panel_date.side_effect = ValueError() with pytest.raises( - vol.MultipleInvalid, + probatio.MultipleInvalid, match=r"datetime must be after 2009 at 'datetime'", ): await hass.services.async_call( diff --git a/tests/components/broadlink/test_helpers.py b/tests/components/broadlink/test_helpers.py index e3ca11a3e2d7ad..5ea3cfd1c67026 100644 --- a/tests/components/broadlink/test_helpers.py +++ b/tests/components/broadlink/test_helpers.py @@ -1,7 +1,7 @@ """Tests for Broadlink helper functions.""" +import probatio import pytest -import voluptuous as vol from homeassistant.components.broadlink.helpers import data_packet, mac_address from homeassistant.core import HomeAssistant @@ -52,5 +52,5 @@ async def test_invalid_mac_address(hass: HomeAssistant) -> None: "a1b-2-c3-d4-e5-fg", ] for mac in invalid: - with pytest.raises((ValueError, vol.Invalid)): + with pytest.raises((ValueError, probatio.Invalid)): mac_address(mac) diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 22d1b3d6e119f0..5b9e1c3767f364 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, MagicMock from bsblan import BSBLANAuthError, BSBLANConnectionError, BSBLANError +import probatio import pytest -import voluptuous as vol from homeassistant.components.bsblan.const import ( CONF_HEATING_CIRCUITS, @@ -327,7 +327,7 @@ async def test_authentication_error( assert passkey_field.default() == "secret" assert username_field.default() == "testuser" # Password should never be pre-filled for security reasons - assert password_field.default is vol.UNDEFINED + assert password_field.default is probatio.UNDEFINED async def test_authentication_error_vs_connection_error( diff --git a/tests/components/bsblan/test_services.py b/tests/components/bsblan/test_services.py index 0e00cc2192b179..987ef96074cc12 100644 --- a/tests/components/bsblan/test_services.py +++ b/tests/components/bsblan/test_services.py @@ -14,8 +14,8 @@ TimeSlot, ) from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components.bsblan.const import DOMAIN from homeassistant.core import HomeAssistant @@ -583,7 +583,7 @@ async def test_non_standard_time_types( ) -> None: """Test service with non-standard time types raises error.""" # Test with integer time values - schema validation will reject these - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( DOMAIN, "set_hot_water_schedule", diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index f97e0ac14a1590..28a9a8109170a5 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -7,9 +7,9 @@ from typing import Any from freezegun import freeze_time +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.calendar import ( CREATE_EVENT_SERVICE, @@ -249,35 +249,35 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: [ ( {}, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at least one of start_date, start_date_time, in", ), ( { "start_date": "2022-04-01", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( { "end_date": "2022-04-02", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at least one of start_date, start_date_time, in.", ), ( { "start_date_time": "2022-04-01T06:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end datetimes must both be specified", ), ( { "end_date_time": "2022-04-02T07:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at least one of start_date, start_date_time, in.", ), ( @@ -286,7 +286,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "start_date_time": "2022-04-01T06:00:00", "end_date_time": "2022-04-02T07:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at most one of start_date, start_date_time, in.", ), ( @@ -295,7 +295,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "end_date_time": "2022-04-01T07:00:00", "end_date": "2022-04-02", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( @@ -303,7 +303,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "start_date": "2022-04-01", "end_date_time": "2022-04-02T07:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( @@ -311,7 +311,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "start_date_time": "2022-04-01T07:00:00", "end_date": "2022-04-02", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( @@ -321,7 +321,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "weeks": 2, } }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "two or more values in the same group of exclusion 'event_types'", ), ( @@ -332,7 +332,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "days": 2, }, }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at most one of start_date, start_date_time, in.", ), ( @@ -343,7 +343,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "days": 2, }, }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at most one of start_date, start_date_time, in.", ), ( @@ -351,7 +351,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "start_date_time": "2022-04-01T06:00:00+00:00", "end_date_time": "2022-04-01T07:00:00+01:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Expected all values to have the same timezone", ), ( @@ -359,7 +359,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "start_date_time": "2022-04-01T07:00:00", "end_date_time": "2022-04-01T06:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Expected minimum event duration", ), ( @@ -367,7 +367,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "start_date": "2022-04-02", "end_date": "2022-04-01", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Expected minimum event duration", ), ( @@ -375,7 +375,7 @@ async def test_unsupported_create_event_service(hass: HomeAssistant) -> None: "start_date": "2022-04-01", "end_date": "2022-04-01", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Expected minimum event duration", ), ], @@ -520,7 +520,7 @@ async def test_list_events_service_duration( async def test_list_events_positive_duration(hass: HomeAssistant) -> None: """Test listing events requires a positive duration.""" - with pytest.raises(vol.Invalid, match="should be positive"): + with pytest.raises(probatio.Invalid, match="should be positive"): await hass.services.async_call( DOMAIN, SERVICE_GET_EVENTS, @@ -537,7 +537,7 @@ async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None: """Test listing events specifying fields that are exclusive.""" end = dt_util.now() + timedelta(days=1) - with pytest.raises(vol.Invalid, match="at most one of"): + with pytest.raises(probatio.Invalid, match="at most one of"): await hass.services.async_call( DOMAIN, SERVICE_GET_EVENTS, @@ -553,7 +553,7 @@ async def test_list_events_exclusive_fields(hass: HomeAssistant) -> None: async def test_list_events_missing_fields(hass: HomeAssistant) -> None: """Test listing events missing some required fields.""" - with pytest.raises(vol.Invalid, match="at least one of"): + with pytest.raises(probatio.Invalid, match="at least one of"): await hass.services.async_call( DOMAIN, SERVICE_GET_EVENTS, @@ -608,7 +608,7 @@ async def test_list_events_service_same_dates( ) -> None: """Test listing events from the service call using the same start and end time.""" - with pytest.raises(vol.error.MultipleInvalid, match=re.escape(error_msg)): + with pytest.raises(probatio.error.MultipleInvalid, match=re.escape(error_msg)): await hass.services.async_call( DOMAIN, SERVICE_GET_EVENTS, diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index a0307c45310e5c..c87e89ec288a1c 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -4,8 +4,8 @@ from typing import Any from unittest.mock import MagicMock, Mock +import probatio import pytest -import voluptuous as vol from homeassistant.components.climate import ( DOMAIN, @@ -58,7 +58,7 @@ async def test_set_temp_schema_no_req( calls = async_mock_service(hass, domain, service, schema) data = {"hvac_mode": "off", "entity_id": ["climate.test_id"]} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call(domain, service, data) await hass.async_block_till_done() diff --git a/tests/components/climate/test_trigger.py b/tests/components/climate/test_trigger.py index 9b9cc69c7504c7..6c6305ee2d510d 100644 --- a/tests/components/climate/test_trigger.py +++ b/tests/components/climate/test_trigger.py @@ -3,8 +3,8 @@ from contextlib import AbstractContextManager, nullcontext as does_not_raise from typing import Any +import probatio import pytest -import voluptuous as vol from homeassistant.components.climate.const import ( ATTR_HUMIDITY, @@ -137,18 +137,18 @@ def test_trigger_target_support() -> None: "climate.hvac_mode_changed", # Empty hvac_mode list {CONF_HVAC_MODE: []}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( "climate.hvac_mode_changed", # Missing CONF_HVAC_MODE {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( "climate.hvac_mode_changed", {CONF_HVAC_MODE: ["invalid_mode"]}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) diff --git a/tests/components/cloud/test_ai_task.py b/tests/components/cloud/test_ai_task.py index b3df11f03c47e6..c3347cfd7c3ad1 100644 --- a/tests/components/cloud/test_ai_task.py +++ b/tests/components/cloud/test_ai_task.py @@ -12,8 +12,8 @@ LLMServiceError, ) from PIL import Image +import probatio import pytest -import voluptuous as vol from homeassistant.components import ai_task, conversation from homeassistant.components.cloud import DOMAIN @@ -191,7 +191,7 @@ async def test_generate_data_returns_json( task = ai_task.GenDataTask( name="Task", instructions="Return JSON", - structure=vol.Schema({vol.Required("names"): [str]}), + structure=probatio.Schema({probatio.Required("names"): [str]}), ) async def fake_handle(chat_type, log, task_name, structure): @@ -219,7 +219,7 @@ async def test_generate_data_invalid_json( task = ai_task.GenDataTask( name="Task", instructions="Return JSON", - structure=vol.Schema({vol.Required("names"): [str]}), + structure=probatio.Schema({probatio.Required("names"): [str]}), ) async def fake_handle(chat_type, log, task_name, structure): diff --git a/tests/components/cloud/test_entity.py b/tests/components/cloud/test_entity.py index fe9c72b181155c..49803cb270f2e2 100644 --- a/tests/components/cloud/test_entity.py +++ b/tests/components/cloud/test_entity.py @@ -6,8 +6,8 @@ from unittest.mock import AsyncMock, MagicMock, patch from PIL import Image +import probatio import pytest -import voluptuous as vol from homeassistant.components import conversation from homeassistant.components.cloud.const import AI_TASK_ENTITY_UNIQUE_ID, DOMAIN @@ -57,7 +57,7 @@ class DummyTool(llm.Tool): name = "do_something" description = "Test tool" - parameters = vol.Schema({vol.Required("value"): str}) + parameters = probatio.Schema({probatio.Required("value"): str}) async def async_call(self, hass: HomeAssistant, tool_input, llm_context): """No-op implementation.""" @@ -66,13 +66,13 @@ async def async_call(self, hass: HomeAssistant, tool_input, llm_context): async def test_format_structured_output() -> None: """Test that structured output schemas are normalized.""" - schema = vol.Schema( + schema = probatio.Schema( { - vol.Required("name"): selector.TextSelector(), - vol.Optional("age"): selector.NumberSelector( + probatio.Required("name"): selector.TextSelector(), + probatio.Optional("age"): selector.NumberSelector( config=selector.NumberSelectorConfig(min=0, max=120), ), - vol.Required("stuff"): selector.ObjectSelector( + probatio.Required("stuff"): selector.ObjectSelector( { "multiple": True, "fields": { diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 9d431d03b96888..a95196d5136e63 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -10,8 +10,8 @@ from hass_nabucasa.voice import VoiceError, VoiceTokenError from hass_nabucasa.voice_data import TTS_VOICES +import probatio import pytest -import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import ( # pylint: disable=home-assistant-component-root-import STORAGE_KEY, @@ -91,12 +91,12 @@ def test_schema() -> None: processed = PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) assert processed["gender"] == "female" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): PLATFORM_SCHEMA( {"platform": "cloud", "language": "non-existing", "gender": "female"} ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): PLATFORM_SCHEMA( {"platform": "cloud", "language": "nl-NL", "gender": "not-supported"} ) diff --git a/tests/components/color_extractor/test_services.py b/tests/components/color_extractor/test_services.py index 2df6d71cab9013..3da91411f7d114 100644 --- a/tests/components/color_extractor/test_services.py +++ b/tests/components/color_extractor/test_services.py @@ -7,8 +7,8 @@ import aiohttp from PIL import UnidentifiedImageError +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components.color_extractor.services import ( ATTR_PATH, diff --git a/tests/components/common.py b/tests/components/common.py index b359e75de34f50..4b988b733c647a 100644 --- a/tests/components/common.py +++ b/tests/components/common.py @@ -10,8 +10,8 @@ import re from typing import Any, TypedDict +import probatio import pytest -import voluptuous as vol from homeassistant.const import ( ATTR_LABEL_ID, @@ -1716,7 +1716,7 @@ async def _validate_condition_options( if valid: await async_validate_condition_config(hass, config) else: - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_condition_config(hass, config) @@ -1836,7 +1836,7 @@ async def _validate_trigger_options( if valid: await async_validate_trigger_config(hass, [trigger_config]) else: - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_trigger_config(hass, [trigger_config]) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 20879037048042..078604830c3296 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -8,9 +8,9 @@ from aiohttp.test_utils import TestClient from freezegun.api import FrozenDateTimeFactory +import probatio import pytest from pytest_unordered import unordered -import voluptuous as vol from homeassistant import config_entries as core_ce, data_entry_flow, loader from homeassistant.components.config import DOMAIN, config_entries @@ -458,13 +458,13 @@ async def test_initialize_flow(hass: HomeAssistant, client: TestClient) -> None: class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): schema = { - vol.Required("username"): str, - vol.Required("password"): str, + probatio.Required("username"): str, + probatio.Required("password"): str, } return self.async_show_form( step_id="user", - data_schema=vol.Schema(schema), + data_schema=probatio.Schema(schema), description_placeholders={ "url": "https://example.com", }, @@ -505,7 +505,7 @@ async def test_initialize_flow_unmet_dependency( """Test unmet dependencies are listed.""" mock_platform(hass, "test.config_flow", None) - config_schema = vol.Schema({"comp_conf": {"hello": str}}, required=True) + config_schema = probatio.Schema({"comp_conf": {"hello": str}}, required=True) mock_integration( hass, MockModule(domain="dependency_1", config_schema=config_schema) ) @@ -540,13 +540,13 @@ async def test_initialize_flow_unauth( class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): schema = { - vol.Required("username"): str, - vol.Required("password"): str, + probatio.Required("username"): str, + probatio.Required("password"): str, } return self.async_show_form( step_id="user", - data_schema=vol.Schema(schema), + data_schema=probatio.Schema(schema), description_placeholders={"url": "https://example.com"}, errors={"username": "Should be unique."}, ) @@ -662,7 +662,7 @@ class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): return self.async_show_form( - step_id="account", data_schema=vol.Schema({"user_title": str}) + step_id="account", data_schema=probatio.Schema({"user_title": str}) ) async def async_step_account(self, user_input=None): @@ -752,7 +752,7 @@ class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): return self.async_show_form( - step_id="account", data_schema=vol.Schema({"user_title": str}) + step_id="account", data_schema=probatio.Schema({"user_title": str}) ) async def async_step_account(self, user_input=None): @@ -883,13 +883,13 @@ async def test_get_progress_flow(hass: HomeAssistant, client: TestClient) -> Non class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): schema = { - vol.Required("username"): str, - vol.Required("password"): str, + probatio.Required("username"): str, + probatio.Required("password"): str, } return self.async_show_form( step_id="user", - data_schema=vol.Schema(schema), + data_schema=probatio.Schema(schema), errors={"username": "Should be unique."}, ) @@ -919,13 +919,13 @@ async def test_get_progress_flow_unauth( class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): schema = { - vol.Required("username"): str, - vol.Required("password"): str, + probatio.Required("username"): str, + probatio.Required("password"): str, } return self.async_show_form( step_id="user", - data_schema=vol.Schema(schema), + data_schema=probatio.Schema(schema), errors={"username": "Should be unique."}, ) @@ -1363,7 +1363,9 @@ class OptionsFlowHandler(data_entry_flow.FlowHandler): async def async_step_init(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("enabled"): bool}), + data_schema=probatio.Schema( + {probatio.Required("enabled"): bool} + ), description_placeholders={"enabled": "Set to true to be true"}, ) @@ -1426,7 +1428,9 @@ class OptionsFlowHandler(data_entry_flow.FlowHandler): async def async_step_init(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("enabled"): bool}), + data_schema=probatio.Schema( + {probatio.Required("enabled"): bool} + ), description_placeholders={"enabled": "Set to true to be true"}, ) @@ -1463,7 +1467,7 @@ def async_get_options_flow(config_entry): class OptionsFlowHandler(data_entry_flow.FlowHandler): async def async_step_init(self, user_input=None): return self.async_show_form( - step_id="finish", data_schema=vol.Schema({"enabled": bool}) + step_id="finish", data_schema=probatio.Schema({"enabled": bool}) ) async def async_step_finish(self, user_input=None): @@ -1532,9 +1536,9 @@ class OptionsFlowHandler(data_entry_flow.FlowHandler): async def async_step_init(self, user_input=None): return self.async_show_form( step_id="finish", - data_schema=vol.Schema( + data_schema=probatio.Schema( { - vol.Required( + probatio.Required( "choices", default=["invalid", "valid"] ): cv.multi_select({"valid": "Valid"}) } @@ -1602,7 +1606,7 @@ async def async_step_init(self, user_input=None): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("enabled"): bool}), + data_schema=probatio.Schema({probatio.Required("enabled"): bool}), description_placeholders={"enabled": "Set to true to be true"}, ) @@ -1664,7 +1668,7 @@ async def async_step_reconfigure(self, user_input=None): return self.async_show_form( step_id="reconfigure", - data_schema=vol.Schema({vol.Required("enabled"): bool}), + data_schema=probatio.Schema({probatio.Required("enabled"): bool}), description_placeholders={"enabled": "Set to true to be true"}, ) @@ -1761,7 +1765,7 @@ async def async_step_finish(self, user_input=None): ) return self.async_show_form( - step_id="finish", data_schema=vol.Schema({"enabled": bool}) + step_id="finish", data_schema=probatio.Schema({"enabled": bool}) ) @classmethod @@ -1897,7 +1901,7 @@ class SubentryFlowHandler(core_ce.ConfigSubentryFlow): async def async_step_init(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("enabled"): bool}), + data_schema=probatio.Schema({probatio.Required("enabled"): bool}), description_placeholders={"enabled": "Set to true to be true"}, ) @@ -1944,7 +1948,7 @@ async def async_step_finish(self, user_input=None): ) return self.async_show_form( - step_id="finish", data_schema=vol.Schema({"enabled": bool}) + step_id="finish", data_schema=probatio.Schema({"enabled": bool}) ) @classmethod @@ -2015,9 +2019,9 @@ class SubentryFlowHandler(core_ce.ConfigSubentryFlow): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="finish", - data_schema=vol.Schema( + data_schema=probatio.Schema( { - vol.Required( + probatio.Required( "choices", default=["invalid", "valid"] ): cv.multi_select({"valid": "Valid"}) } @@ -3355,11 +3359,13 @@ class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema( + data_schema=probatio.Schema( { - vol.Required(CONF_LATITUDE): cv.latitude, - vol.Required(CONF_LONGITUDE): cv.longitude, - vol.Required(CONF_RADIUS): vol.All(int, vol.Range(min=5)), + probatio.Required(CONF_LATITUDE): cv.latitude, + probatio.Required(CONF_LONGITUDE): cv.longitude, + probatio.Required(CONF_RADIUS): probatio.All( + int, probatio.Range(min=5) + ), } ), ) @@ -3399,9 +3405,9 @@ class TestFlow(core_ce.ConfigFlow): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema( + data_schema=probatio.Schema( { - vol.Required(CONF_LATITUDE): cv.latitude, + probatio.Required(CONF_LATITUDE): cv.latitude, } ), ) @@ -3457,7 +3463,7 @@ async def async_step_user(self, user_input=None): async def async_step_reconfigure(self, user_input=None): if user_input is None: return self.async_show_form( - step_id="reconfigure", data_schema=vol.Schema({}) + step_id="reconfigure", data_schema=probatio.Schema({}) ) return self.async_update_reload_and_abort( self._get_reconfigure_entry(), diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 19e64adb53d126..83ed632784ab3e 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -52,8 +52,8 @@ from aiohasupervisor.resolution import ResolutionClient from aiohasupervisor.store import StoreClient from aiohasupervisor.supervisor import SupervisorManagementClient +import probatio import pytest -import voluptuous as vol from homeassistant import components, loader from homeassistant.components import repairs @@ -1061,7 +1061,7 @@ async def _check_step_or_section_translations( integration: str, translation_prefix: str, description_placeholders: dict[str, str], - data_schema: vol.Schema | None, + data_schema: probatio.Schema | None, ignore_translations_for_mock_domains: set[str], ) -> None: # neither title nor description are required diff --git a/tests/components/conversation/test_chat_log.py b/tests/components/conversation/test_chat_log.py index 1af92459d04726..cbe22e8ae5e8b8 100644 --- a/tests/components/conversation/test_chat_log.py +++ b/tests/components/conversation/test_chat_log.py @@ -7,9 +7,9 @@ from unittest.mock import AsyncMock, Mock, patch from freezegun import freeze_time +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.conversation import ( AssistantContent, @@ -142,8 +142,8 @@ class MyTool(llm.Tool): name = "test_tool" description = "Test function" - parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) class MyAPI(llm.API): @@ -437,8 +437,8 @@ async def test_tool_call( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) mock_tool.async_call.return_value = "Test response" @@ -515,8 +515,8 @@ async def test_tool_call_exception( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) mock_tool.async_call.side_effect = HomeAssistantError("Test error") @@ -696,8 +696,8 @@ async def test_add_delta_content_stream( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) async def tool_call( diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 0b38ee3cf27036..ae745127cd2b72 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -3,9 +3,9 @@ from http import HTTPStatus from unittest.mock import patch +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import conversation from homeassistant.components.conversation import ( @@ -207,7 +207,7 @@ async def test_agent_id_validator_invalid_agent( hass: HomeAssistant, init_components ) -> None: """Test validating agent id.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): conversation.agent_id_validator("invalid_agent") conversation.agent_id_validator(conversation.HOME_ASSISTANT_AGENT) diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 1f9e54a38a9d1d..8ec180347e4382 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -2,8 +2,8 @@ import logging +import probatio import pytest -import voluptuous as vol from homeassistant.components.conversation import HOME_ASSISTANT_AGENT, async_get_agent from homeassistant.components.conversation.models import ConversationInput @@ -522,7 +522,7 @@ async def test_same_sentence_multiple_triggers( ) async def test_fails_on_punctuation(hass: HomeAssistant, command: str) -> None: """Test that validation fails when sentences contain punctuation.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await trigger.async_validate_trigger_config( hass, [ @@ -543,7 +543,7 @@ async def test_fails_on_punctuation(hass: HomeAssistant, command: str) -> None: ) async def test_fails_on_empty(hass: HomeAssistant, command: str) -> None: """Test that validation fails when sentences are empty.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await trigger.async_validate_trigger_config( hass, [ @@ -560,7 +560,7 @@ async def test_fails_on_empty(hass: HomeAssistant, command: str) -> None: async def test_fails_on_no_sentences(hass: HomeAssistant) -> None: """Test that validation fails when no sentences are provided.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await trigger.async_validate_trigger_config( hass, [ @@ -575,7 +575,7 @@ async def test_fails_on_no_sentences(hass: HomeAssistant) -> None: async def test_fails_on_bad_parse(hass: HomeAssistant) -> None: """Test that validation fails when sentence is malformed.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await trigger.async_validate_trigger_config( hass, [ diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index a0ff06f66fabd5..674793e0f7f597 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -3,9 +3,9 @@ from collections.abc import Callable from typing import Any +import probatio from pydeconz.errors import RequestError import pytest -import voluptuous as vol from homeassistant.components.deconz.const import ( CONF_BRIDGE_ID, @@ -142,7 +142,7 @@ async def test_configure_service_with_faulty_field(hass: HomeAssistant) -> None: """Test that service fails on a bad field.""" data = {SERVICE_FIELD: "light/2", SERVICE_DATA: {}} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_CONFIGURE_DEVICE, service_data=data ) diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 42152645ecbe23..36fe15e919a60c 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -3,8 +3,8 @@ from collections.abc import Generator from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, @@ -104,7 +104,7 @@ async def test_set_only_target_temp_bad_attr(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_TEMPERATURE) == 21 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -179,7 +179,7 @@ async def test_set_target_temp_range_bad_attr(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 21.0 assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 24.0 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -224,7 +224,7 @@ async def test_set_target_humidity_bad_attr(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_HUMIDITY) == 67.4 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HUMIDITY, @@ -257,7 +257,7 @@ async def test_set_fan_mode_bad_attr(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_FAN_MODE) == "on_high" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, @@ -290,7 +290,7 @@ async def test_set_swing_mode_bad_attr(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_SWING_MODE) == "off" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, @@ -327,7 +327,7 @@ async def test_set_hvac_bad_attr_and_state(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.COOLING assert state.state == HVACMode.COOL - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, diff --git a/tests/components/demo/test_humidifier.py b/tests/components/demo/test_humidifier.py index 93bd2b13743fc0..2a8f2a7455dd61 100644 --- a/tests/components/demo/test_humidifier.py +++ b/tests/components/demo/test_humidifier.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.humidifier import ( ATTR_ACTION, @@ -74,7 +74,7 @@ async def test_set_target_humidity_bad_attr(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_DEHUMIDIFIER) assert state.attributes.get(ATTR_HUMIDITY) == 54.2 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( HUMIDITY_DOMAIN, SERVICE_SET_HUMIDITY, diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 112af447333bfd..8d83c6262c149e 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -3,8 +3,8 @@ from http import HTTPStatus from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.media_player import ( ATTR_GROUP_MEMBERS, @@ -91,7 +91,7 @@ async def test_source_select(hass: HomeAssistant) -> None: state = hass.states.get(entity_id) assert state.attributes.get(ATTR_INPUT_SOURCE) == "dvd" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -165,7 +165,7 @@ async def test_volume_services(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 1.0 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_SET, @@ -208,7 +208,7 @@ async def test_volume_services(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is False - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_MUTE, @@ -412,7 +412,7 @@ async def test_play_media(hass: HomeAssistant) -> None: ) assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) is not None - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -459,7 +459,7 @@ async def test_seek(hass: HomeAssistant, mock_media_seek) -> None: assert state.attributes[ATTR_SUPPORTED_FEATURES] & MediaPlayerEntityFeature.SEEK assert not mock_media_seek.called - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_SEEK, diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 4b7cbe4864f776..63321893ef1299 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -3,8 +3,8 @@ from collections.abc import Generator from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.number import ( ATTR_MAX, @@ -83,7 +83,7 @@ async def test_set_value_bad_attr(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_VOLUME) assert state.state == "42.0" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/demo/test_water_heater.py b/tests/components/demo/test_water_heater.py index 257e1ab5ffb1a1..46ecbd8c661cf7 100644 --- a/tests/components/demo/test_water_heater.py +++ b/tests/components/demo/test_water_heater.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import water_heater from homeassistant.const import Platform @@ -57,7 +57,7 @@ async def test_set_only_target_temp_bad_attr(hass: HomeAssistant) -> None: """Test setting the target temperature without required attribute.""" state = hass.states.get(ENTITY_WATER_HEATER) assert state.attributes.get("temperature") == 119 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await common.async_set_temperature(hass, None, ENTITY_WATER_HEATER) assert state.attributes.get("temperature") == 119 @@ -79,7 +79,7 @@ async def test_set_operation_bad_attr_and_state(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_WATER_HEATER) assert state.attributes.get("operation_mode") == "eco" assert state.state == "eco" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await common.async_set_operation_mode(hass, None, ENTITY_WATER_HEATER) state = hass.states.get(ENTITY_WATER_HEATER) assert state.attributes.get("operation_mode") == "eco" @@ -101,7 +101,7 @@ async def test_set_away_mode_bad_attr(hass: HomeAssistant) -> None: """Test setting the away mode without required attribute.""" state = hass.states.get(ENTITY_WATER_HEATER) assert state.attributes.get("away_mode") == "off" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await common.async_set_away_mode(hass, None, ENTITY_WATER_HEATER) assert state.attributes.get("away_mode") == "off" diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index ba5d1b9fcee78a..76d240da5d3fc2 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -4,9 +4,9 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import attr +import probatio import pytest from pytest_unordered import unordered -import voluptuous as vol from homeassistant import loader from homeassistant.components import automation, device_automation @@ -75,7 +75,7 @@ async def _async_get_triggers( f"{FAKE_DOMAIN}.device_action", Mock( ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend( - {vol.Required("domain"): FAKE_DOMAIN} + {probatio.Required("domain"): FAKE_DOMAIN} ), async_get_actions=_async_get_actions, spec=["ACTION_SCHEMA", "async_get_actions"], @@ -87,7 +87,7 @@ async def _async_get_triggers( f"{FAKE_DOMAIN}.device_condition", Mock( CONDITION_SCHEMA=toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required("domain"): FAKE_DOMAIN} + {probatio.Required("domain"): FAKE_DOMAIN} ), async_get_conditions=_async_get_conditions, spec=["CONDITION_SCHEMA", "async_get_conditions"], @@ -98,10 +98,11 @@ async def _async_get_triggers( hass, f"{FAKE_DOMAIN}.device_trigger", Mock( - TRIGGER_SCHEMA=vol.All( + TRIGGER_SCHEMA=probatio.All( toggle_entity.TRIGGER_SCHEMA, - vol.Schema( - {vol.Required("domain"): FAKE_DOMAIN}, extra=vol.ALLOW_EXTRA + probatio.Schema( + {probatio.Required("domain"): FAKE_DOMAIN}, + extra=probatio.ALLOW_EXTRA, ), ), async_get_triggers=_async_get_triggers, @@ -311,10 +312,10 @@ async def test_websocket_get_action_capabilities( async def _async_get_action_capabilities( hass: HomeAssistant, config: ConfigType - ) -> dict[str, vol.Schema]: + ) -> dict[str, probatio.Schema]: """List action capabilities.""" if config["type"] == "turn_on": - return {"extra_fields": vol.Schema({vol.Optional("code"): str})} + return {"extra_fields": probatio.Schema({probatio.Optional("code"): str})} return {} module_cache = hass.data[loader.DATA_COMPONENTS] @@ -473,7 +474,7 @@ async def test_websocket_get_condition_capabilities( async def _async_get_condition_capabilities( hass: HomeAssistant, config: ConfigType - ) -> dict[str, vol.Schema]: + ) -> dict[str, probatio.Schema]: """List condition capabilities.""" return await toggle_entity.async_get_condition_capabilities(hass, config) @@ -771,7 +772,7 @@ async def test_websocket_get_trigger_capabilities( async def _async_get_trigger_capabilities( hass: HomeAssistant, config: ConfigType - ) -> dict[str, vol.Schema]: + ) -> dict[str, probatio.Schema]: """List trigger capabilities.""" return await toggle_entity.async_get_trigger_capabilities(hass, config) @@ -1926,9 +1927,9 @@ async def test_validate_config_rewrites_composite_device_id( "entity_id": entity.entity_id, "type": "turned_on", }, - vol.Schema( - {vol.Required("device_id"): str, vol.Required("domain"): str}, - extra=vol.ALLOW_EXTRA, + probatio.Schema( + {probatio.Required("device_id"): str, probatio.Required("domain"): str}, + extra=probatio.ALLOW_EXTRA, ), DeviceAutomationType.TRIGGER, ) diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 33651c4a65481b..35241fd74f85a9 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -754,7 +754,7 @@ async def test_options_flow( assert result["step_id"] == "init" assert result["errors"] == {} - # Invalid URL for callback (can't be validated automatically by voluptuous) + # Invalid URL for callback (can't be validated automatically by probatio) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/downloader/test_services.py b/tests/components/downloader/test_services.py index c9ffdf6cccd967..b3ee25c4bad5fa 100644 --- a/tests/components/downloader/test_services.py +++ b/tests/components/downloader/test_services.py @@ -3,10 +3,10 @@ import asyncio from contextlib import AbstractContextManager, nullcontext as does_not_raise +import probatio import pytest import requests from requests_mock import Mocker -import voluptuous as vol from homeassistant.components.downloader.const import DOMAIN from homeassistant.core import HomeAssistant @@ -84,17 +84,17 @@ async def test_download_headers_passed_through( @pytest.mark.parametrize( ("headers", "expected_result"), [ - (1, pytest.raises(vol.error.Invalid)), # Not a dictionary + (1, pytest.raises(probatio.error.Invalid)), # Not a dictionary ({"Accept": "application/json"}, does_not_raise()), ({123: 456.789}, does_not_raise()), # Convert numbers to strings ( {"Accept": ["application/json"]}, - pytest.raises(vol.error.MultipleInvalid), + pytest.raises(probatio.error.MultipleInvalid), ), # Value is not a string - ({1: None}, pytest.raises(vol.error.MultipleInvalid)), # Value is None + ({1: None}, pytest.raises(probatio.error.MultipleInvalid)), # Value is None ( {None: "application/json"}, - pytest.raises(vol.error.MultipleInvalid), + pytest.raises(probatio.error.MultipleInvalid), ), # Key is None ], ) diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index 3335e12b2a2043..2994fc6ac07678 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import call, patch +from probatio import MultipleInvalid import pytest -from voluptuous import MultipleInvalid from homeassistant.components.dynalite import const as dynalite from homeassistant.const import CONF_HOST diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py index 1379b49d55e281..2790fa0be68a41 100644 --- a/tests/components/eafm/test_config_flow.py +++ b/tests/components/eafm/test_config_flow.py @@ -2,8 +2,8 @@ from unittest.mock import patch +from probatio.error import Invalid import pytest -from voluptuous.error import Invalid from homeassistant import config_entries from homeassistant.components.eafm import const diff --git a/tests/components/easyenergy/test_services.py b/tests/components/easyenergy/test_services.py index 652fe384cc9adf..184422bb03dd84 100644 --- a/tests/components/easyenergy/test_services.py +++ b/tests/components/easyenergy/test_services.py @@ -10,9 +10,9 @@ ElectricityGranularity, VatOption, ) +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.easyenergy.const import DOMAIN from homeassistant.components.easyenergy.services import ( @@ -285,7 +285,7 @@ async def test_service_schema_validation( ) -> None: """Test easyEnergy service schema validation.""" - with pytest.raises(vol.error.Error, match=error_message): + with pytest.raises(probatio.error.Error, match=error_message): await hass.services.async_call( DOMAIN, service, @@ -313,7 +313,7 @@ async def test_service_schema_validation_vat( ) -> None: """Test easyEnergy service schema validation for VAT.""" - with pytest.raises(vol.error.Error, match=error_message): + with pytest.raises(probatio.error.Error, match=error_message): await hass.services.async_call( DOMAIN, service, @@ -347,7 +347,7 @@ async def test_service_schema_validation_usage_price_type( ) -> None: """Test usage service schema validation for price type.""" - with pytest.raises(vol.error.Error, match=error_message): + with pytest.raises(probatio.error.Error, match=error_message): await hass.services.async_call( DOMAIN, service, @@ -390,7 +390,7 @@ async def test_service_schema_validation_granularity( if service == ENERGY_USAGE_SERVICE_NAME: data["incl_vat"] = True - with pytest.raises(vol.error.Error, match=error_message): + with pytest.raises(probatio.error.Error, match=error_message): await hass.services.async_call( DOMAIN, service, @@ -407,7 +407,7 @@ async def test_service_schema_validation_return_vat( ) -> None: """Test return prices do not accept VAT selection.""" - with pytest.raises(vol.error.Error, match="not a valid option .+"): + with pytest.raises(probatio.error.Error, match="not a valid option .+"): await hass.services.async_call( DOMAIN, ENERGY_RETURN_SERVICE_NAME, diff --git a/tests/components/ecobee/test_util.py b/tests/components/ecobee/test_util.py index 88032362af00a8..472fbe054194ae 100644 --- a/tests/components/ecobee/test_util.py +++ b/tests/components/ecobee/test_util.py @@ -1,7 +1,7 @@ """Tests for the ecobee.util module.""" +import probatio import pytest -import voluptuous as vol from homeassistant.components.ecobee.util import ecobee_date, ecobee_time @@ -17,7 +17,7 @@ async def test_ecobee_date_with_invalid_input() -> None: """Test that the date function raises the expected exception.""" test_input = "20190927" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ecobee_date(test_input) @@ -32,5 +32,5 @@ async def test_ecobee_time_with_invalid_input() -> None: """Test that the time function raises the expected exception.""" test_input = "20:55" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ecobee_time(test_input) diff --git a/tests/components/energy/test_data.py b/tests/components/energy/test_data.py index 2cb22dd25fdd84..cfa7f827528bb1 100644 --- a/tests/components/energy/test_data.py +++ b/tests/components/energy/test_data.py @@ -1,7 +1,7 @@ """Test energy data storage and migration.""" +import probatio import pytest -import voluptuous as vol from homeassistant.components.energy.data import ( ENERGY_SOURCE_SCHEMA, @@ -196,9 +196,9 @@ async def test_battery_capacity_round_trip( assert manager.data is not None assert manager.data["energy_sources"][0]["capacity"] == 13.5 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ENERGY_SOURCE_SCHEMA([{**battery_source, "capacity": 0}]) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ENERGY_SOURCE_SCHEMA([{**battery_source, "capacity": -1}]) @@ -324,14 +324,16 @@ async def test_power_config_takes_precedence_over_stat_rate( async def test_power_config_validation_empty() -> None: """Test that empty power_config raises validation error.""" - with pytest.raises(vol.Invalid, match="power_config must have at least one option"): + with pytest.raises( + probatio.Invalid, match="power_config must have at least one option" + ): POWER_CONFIG_SCHEMA({}) async def test_power_config_validation_multiple_methods() -> None: """Test that power_config with multiple methods raises validation error.""" # Both stat_rate and stat_rate_inverted (should fail due to Exclusive) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): POWER_CONFIG_SCHEMA( { "stat_rate": "sensor.power", @@ -340,7 +342,7 @@ async def test_power_config_validation_multiple_methods() -> None: ) # Both stat_rate and stat_rate_from/to (should fail due to Exclusive) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): POWER_CONFIG_SCHEMA( { "stat_rate": "sensor.power", @@ -350,7 +352,7 @@ async def test_power_config_validation_multiple_methods() -> None: ) # Both stat_rate_inverted and stat_rate_from/to (should fail due to Exclusive) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): POWER_CONFIG_SCHEMA( { "stat_rate_inverted": "sensor.power", @@ -364,7 +366,8 @@ async def test_flow_from_validation_multiple_prices() -> None: """Test that flow_from validation rejects both entity and number price.""" # Both entity_energy_price and number_energy_price should fail with pytest.raises( - vol.Invalid, match="Define either an entity or a fixed number for the price" + probatio.Invalid, + match="Define either an entity or a fixed number for the price", ): FLOW_FROM_GRID_SOURCE_SCHEMA( { @@ -876,7 +879,8 @@ async def test_grid_new_format_no_migration_needed(hass: HomeAssistant) -> None: async def test_grid_validation_single_import_price() -> None: """Test that grid validation rejects both entity and number import price.""" with pytest.raises( - vol.Invalid, match="Define either an entity or a fixed number for import price" + probatio.Invalid, + match="Define either an entity or a fixed number for import price", ): ENERGY_SOURCE_SCHEMA( [ @@ -894,7 +898,8 @@ async def test_grid_validation_single_import_price() -> None: async def test_grid_validation_single_export_price() -> None: """Test that grid validation rejects both entity and number export price.""" with pytest.raises( - vol.Invalid, match="Define either an entity or a fixed number for export price" + probatio.Invalid, + match="Define either an entity or a fixed number for export price", ): ENERGY_SOURCE_SCHEMA( [ @@ -912,7 +917,7 @@ async def test_grid_validation_single_export_price() -> None: async def test_flow_from_rejects_entity_price_for_external_stat() -> None: """Test that entity_energy_price is rejected for external statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): FLOW_FROM_GRID_SOURCE_SCHEMA( { "stat_energy_from": "opower:utility_elec_12345_energy_consumption", @@ -923,7 +928,7 @@ async def test_flow_from_rejects_entity_price_for_external_stat() -> None: async def test_flow_from_rejects_number_price_for_external_stat() -> None: """Test that number_energy_price is rejected for external statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): FLOW_FROM_GRID_SOURCE_SCHEMA( { "stat_energy_from": "opower:utility_elec_12345_energy_consumption", @@ -960,7 +965,7 @@ async def test_flow_from_allows_no_cost_for_external_stat() -> None: async def test_flow_to_rejects_entity_price_for_external_stat() -> None: """Test that entity_energy_price is rejected for external export statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): FLOW_TO_GRID_SOURCE_SCHEMA( { "stat_energy_to": "external:grid_export", @@ -971,7 +976,7 @@ async def test_flow_to_rejects_entity_price_for_external_stat() -> None: async def test_flow_to_rejects_number_price_for_external_stat() -> None: """Test that number_energy_price is rejected for external export statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): FLOW_TO_GRID_SOURCE_SCHEMA( { "stat_energy_to": "external:grid_export", @@ -982,7 +987,7 @@ async def test_flow_to_rejects_number_price_for_external_stat() -> None: async def test_grid_rejects_entity_price_for_external_import_stat() -> None: """Test that grid schema rejects entity price for external import stats.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): ENERGY_SOURCE_SCHEMA( [ { @@ -997,7 +1002,7 @@ async def test_grid_rejects_entity_price_for_external_import_stat() -> None: async def test_grid_rejects_number_price_for_external_export_stat() -> None: """Test that grid schema rejects number price for external export stats.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): ENERGY_SOURCE_SCHEMA( [ { @@ -1030,7 +1035,7 @@ async def test_grid_allows_stat_cost_for_external_stat() -> None: async def test_gas_rejects_entity_price_for_external_stat() -> None: """Test that gas schema rejects entity price for external statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): GAS_SOURCE_SCHEMA( { "type": "gas", @@ -1042,7 +1047,7 @@ async def test_gas_rejects_entity_price_for_external_stat() -> None: async def test_gas_rejects_number_price_for_external_stat() -> None: """Test that gas schema rejects number price for external statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): GAS_SOURCE_SCHEMA( { "type": "gas", @@ -1054,7 +1059,7 @@ async def test_gas_rejects_number_price_for_external_stat() -> None: async def test_water_rejects_entity_price_for_external_stat() -> None: """Test that water schema rejects entity price for external statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): WATER_SOURCE_SCHEMA( { "type": "water", @@ -1066,7 +1071,7 @@ async def test_water_rejects_entity_price_for_external_stat() -> None: async def test_water_rejects_number_price_for_external_stat() -> None: """Test that water schema rejects number price for external statistics.""" - with pytest.raises(vol.Invalid, match="not supported for external statistics"): + with pytest.raises(probatio.Invalid, match="not supported for external statistics"): WATER_SOURCE_SCHEMA( { "type": "water", diff --git a/tests/components/energyzero/test_services.py b/tests/components/energyzero/test_services.py index 04656d011bc24a..1f62388a68d7c3 100644 --- a/tests/components/energyzero/test_services.py +++ b/tests/components/energyzero/test_services.py @@ -12,9 +12,9 @@ PriceType, TimeRange, ) +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.energyzero.const import ( CONF_ELECTRICITY_PRICE_INTERVAL, @@ -409,23 +409,23 @@ def config_entry_data( @pytest.mark.parametrize( ("config_entry_data", "service_data", "error", "error_message"), [ - ({}, {}, vol.error.Error, "required key not provided .+"), + ({}, {}, probatio.error.Error, "required key not provided .+"), ( {"config_entry": True}, {}, - vol.error.Error, + probatio.error.Error, "required key not provided .+", ), ( {}, {"incl_vat": True}, - vol.error.Error, + probatio.error.Error, "required key not provided .+", ), ( {"config_entry": True}, {"incl_vat": "incorrect vat"}, - vol.error.Error, + probatio.error.Error, "expected bool at .+", ), ( @@ -803,7 +803,7 @@ async def test_service_rejects_unsupported_options( ) -> None: """Only the electricity action accepts the supported new field values.""" mock_energyzero.reset_mock() - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, service, diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index 0aa132220cc16b..3023a223254420 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -5,8 +5,8 @@ import xml.etree.ElementTree as ET import aiohttp +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.environment_canada.const import ( @@ -254,7 +254,7 @@ async def _setup_with_options( return ecmap -def _section_field_names(data_schema: vol.Schema, section_key: str) -> set[str]: +def _section_field_names(data_schema: probatio.Schema, section_key: str) -> set[str]: """Return the field names nested inside a given section of a data schema.""" for key, value in data_schema.schema.items(): if str(key) == section_key: @@ -262,7 +262,7 @@ def _section_field_names(data_schema: vol.Schema, section_key: str) -> set[str]: raise KeyError(section_key) -def _section_defaults(data_schema: vol.Schema, section_key: str) -> dict[str, Any]: +def _section_defaults(data_schema: probatio.Schema, section_key: str) -> dict[str, Any]: """Return the default values nested inside a given section of a data schema.""" for key, value in data_schema.schema.items(): if str(key) == section_key: diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 6f12a79ce46fa2..00ec37186c78b7 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -44,8 +44,8 @@ ) import aiohttp from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.esphome.config_flow import PROBE_NOISE_PSK @@ -1102,7 +1102,7 @@ async def test_esphome_device_service_call_with_validation_error( # Register a service that validates input async def _mock_service(call: ServiceCall) -> None: - raise vol.Invalid("Invalid input provided") + raise probatio.Invalid("Invalid input provided") hass.services.async_register(DOMAIN, "validate_test", _mock_service) diff --git a/tests/components/eurotronic_cometblue/test_config_flow.py b/tests/components/eurotronic_cometblue/test_config_flow.py index 7d513382c1b23a..ff78a222c64a26 100644 --- a/tests/components/eurotronic_cometblue/test_config_flow.py +++ b/tests/components/eurotronic_cometblue/test_config_flow.py @@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, patch from bleak.exc import BleakDeviceNotFoundError +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.eurotronic_cometblue.config_flow import ( @@ -53,7 +53,7 @@ async def test_user_step_discovered_devices( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_device" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADDRESS: "wrong_address"} ) diff --git a/tests/components/eurotronic_cometblue/test_services.py b/tests/components/eurotronic_cometblue/test_services.py index 77a190f10167f7..ef22cceb7f4079 100644 --- a/tests/components/eurotronic_cometblue/test_services.py +++ b/tests/components/eurotronic_cometblue/test_services.py @@ -1,9 +1,9 @@ """Test eurotronic_cometblue services.""" from freezegun import freeze_time +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.eurotronic_cometblue import DOMAIN from homeassistant.components.number import ServiceValidationError @@ -121,8 +121,8 @@ async def test_set_schedule_errors( """Test set_schedule service error handling.""" await setup_with_selected_platforms(hass, mock_config_entry) - # voloptuous schema should catch invalid time formats and incorrect data - with pytest.raises(vol.Invalid, match="Invalid time specified"): + # probatio schema should catch invalid time formats and incorrect data + with pytest.raises(probatio.Invalid, match="Invalid time specified"): await hass.services.async_call( DOMAIN, "set_schedule", @@ -133,7 +133,7 @@ async def test_set_schedule_errors( blocking=True, ) - with pytest.raises(vol.Invalid, match="expected a list at 'monday'"): + with pytest.raises(probatio.Invalid, match="expected a list at 'monday'"): await hass.services.async_call( DOMAIN, "set_schedule", @@ -144,7 +144,7 @@ async def test_set_schedule_errors( blocking=True, ) - with pytest.raises(vol.Invalid, match="expected a list at 'monday'"): + with pytest.raises(probatio.Invalid, match="expected a list at 'monday'"): await hass.services.async_call( DOMAIN, "set_schedule", @@ -155,7 +155,7 @@ async def test_set_schedule_errors( blocking=True, ) - with pytest.raises(vol.Invalid, match="length of value must be at most 4"): + with pytest.raises(probatio.Invalid, match="length of value must be at most 4"): await hass.services.async_call( DOMAIN, "set_schedule", @@ -172,7 +172,7 @@ async def test_set_schedule_errors( blocking=True, ) - # Errors not caught by voluptous schema + # Errors not caught by probatio schema with pytest.raises(ServiceValidationError, match="Missing from/to in entry"): await hass.services.async_call( DOMAIN, diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index 26a5eaa1edab35..6c8b5437aa783a 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -1,7 +1,7 @@ """Test the services for the Flo by Moen integration.""" +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components.flo.const import DOMAIN from homeassistant.components.flo.switch import ( diff --git a/tests/components/fritz/test_services.py b/tests/components/fritz/test_services.py index 3c6a6b5d8ec95a..f165406044837c 100644 --- a/tests/components/fritz/test_services.py +++ b/tests/components/fritz/test_services.py @@ -7,8 +7,8 @@ FritzConnectionException, FritzServiceError, ) +from probatio import MultipleInvalid import pytest -from voluptuous import MultipleInvalid from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.fritz.services import ( diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 1a8ec57c84a0e3..d8431cf3c486db 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -10,8 +10,8 @@ from aiohttp.test_utils import TestClient from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components.frontend import ( CONF_DEVELOPMENT_PR, @@ -350,7 +350,7 @@ async def test_themes_set_theme_wrong_name( """Test frontend.set_theme service called with wrong name.""" with pytest.raises( - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, match="Theme wrong not found", ): await hass.services.async_call( @@ -454,7 +454,7 @@ async def test_themes_set_dark_theme_wrong_name( ) -> None: """Test frontend.set_theme service called with mode dark and wrong name.""" with pytest.raises( - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, match="Theme wrong not found", ): await hass.services.async_call(DOMAIN, "set_theme", schema, blocking=True) @@ -477,7 +477,7 @@ async def test_themes_reload_themes( return_value={DOMAIN: {CONF_THEMES: {"sad": {"primary-color": "blue"}}}}, ): with pytest.raises( - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, match="Theme happy not found", ): await hass.services.async_call( @@ -1185,13 +1185,13 @@ async def test_development_pr_and_github_token_inclusive() -> None: CONF_DEVELOPMENT_PR: 12345, } } - with pytest.raises(vol.Invalid, match="some but not all"): + with pytest.raises(probatio.Invalid, match="some but not all"): CONFIG_SCHEMA(invalid_config_pr_only) invalid_config_token_only: dict[str, dict[str, Any]] = { DOMAIN: {CONF_GITHUB_TOKEN: "test_token"} } - with pytest.raises(vol.Invalid, match="some but not all"): + with pytest.raises(probatio.Invalid, match="some but not all"): CONFIG_SCHEMA(invalid_config_token_only) diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index b9223b3c620ee5..a633ac768a2fa5 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -4,8 +4,8 @@ from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant import core as ha from homeassistant.components import input_boolean, switch @@ -351,7 +351,7 @@ async def test_set_target_humidity(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get(ENTITY) assert state.attributes.get("humidity") == 40 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( HUMIDIFIER_DOMAIN, SERVICE_SET_HUMIDITY, diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 8f5c9fa580e033..356d3770b08605 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -5,8 +5,8 @@ from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant import config as hass_config, core as ha from homeassistant.components import input_boolean, switch @@ -317,7 +317,7 @@ async def test_set_target_temp(hass: HomeAssistant) -> None: await common.async_set_temperature(hass, 30) state = hass.states.get(ENTITY) assert state.attributes.get("temperature") == 30.0 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await common.async_set_temperature(hass, None) state = hass.states.get(ENTITY) assert state.attributes.get("temperature") == 30.0 diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index f2c3cb4c2d4e7c..7ef497bc6b8e45 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -10,8 +10,8 @@ from aiohttp.client_exceptions import ClientError from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components.google import DOMAIN from homeassistant.components.google.calendar import SERVICE_CREATE_EVENT @@ -289,35 +289,35 @@ async def test_multiple_config_entries( [ ( {}, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at least one of start_date, start_date_time, in", ), ( { "start_date": "2022-04-01", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( { "end_date": "2022-04-02", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at least one of start_date, start_date_time, in.", ), ( { "start_date_time": "2022-04-01T06:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end datetimes must both be specified", ), ( { "end_date_time": "2022-04-02T07:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at least one of start_date, start_date_time, in.", ), ( @@ -326,7 +326,7 @@ async def test_multiple_config_entries( "start_date_time": "2022-04-01T06:00:00", "end_date_time": "2022-04-02T07:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at most one of start_date, start_date_time, in.", ), ( @@ -335,7 +335,7 @@ async def test_multiple_config_entries( "end_date_time": "2022-04-01T07:00:00", "end_date": "2022-04-02", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( @@ -343,7 +343,7 @@ async def test_multiple_config_entries( "start_date": "2022-04-01", "end_date_time": "2022-04-02T07:00:00", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( @@ -351,7 +351,7 @@ async def test_multiple_config_entries( "start_date_time": "2022-04-01T07:00:00", "end_date": "2022-04-02", }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "Start and end dates must both be specified", ), ( @@ -361,7 +361,7 @@ async def test_multiple_config_entries( "weeks": 2, } }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "two or more values in the same group of exclusion 'event_types'", ), ( @@ -372,7 +372,7 @@ async def test_multiple_config_entries( "days": 2, }, }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at most one of start_date, start_date_time, in.", ), ( @@ -383,7 +383,7 @@ async def test_multiple_config_entries( "days": 2, }, }, - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, "must contain at most one of start_date, start_date_time, in.", ), ], diff --git a/tests/components/google_generative_ai_conversation/test_ai_task.py b/tests/components/google_generative_ai_conversation/test_ai_task.py index 5b38591678e3ea..71cff4610b7e43 100644 --- a/tests/components/google_generative_ai_conversation/test_ai_task.py +++ b/tests/components/google_generative_ai_conversation/test_ai_task.py @@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, Mock, patch from google.genai.types import File, FileState, GenerateContentResponse +import probatio import pytest -import voluptuous as vol from homeassistant.components import ai_task, media_source from homeassistant.components.google_generative_ai_conversation.const import ( @@ -174,9 +174,9 @@ async def test_generate_data( task_name="Test Task", entity_id=entity_id, instructions="Give me 2 mario characters", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -217,7 +217,7 @@ async def test_generate_data( task_name="Test Task", entity_id=entity_id, instructions="Test prompt", - structure=vol.Schema({vol.Required("bla"): str}), + structure=probatio.Schema({probatio.Required("bla"): str}), ) diff --git a/tests/components/google_mail/test_notify.py b/tests/components/google_mail/test_notify.py index d5bd174d4a2aec..1d7db1884885f5 100644 --- a/tests/components/google_mail/test_notify.py +++ b/tests/components/google_mail/test_notify.py @@ -2,8 +2,8 @@ from unittest.mock import patch +from probatio.error import Invalid import pytest -from voluptuous.error import Invalid from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant @@ -82,11 +82,11 @@ async def test_notify( assert len(mock_client.mock_calls) == 5 -async def test_notify_voluptuous_error( +async def test_notify_probatio_error( hass: HomeAssistant, setup_integration: ComponentSetup, ) -> None: - """Test voluptuous error thrown when drafting email.""" + """Test probatio error thrown when drafting email.""" await setup_integration() with pytest.raises(ValueError) as ex: diff --git a/tests/components/green_planet_energy/test_services.py b/tests/components/green_planet_energy/test_services.py index 49c4988573e31f..a0afdc9f5539cc 100644 --- a/tests/components/green_planet_energy/test_services.py +++ b/tests/components/green_planet_energy/test_services.py @@ -3,9 +3,9 @@ from unittest.mock import MagicMock from freezegun import freeze_time +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.green_planet_energy.const import DOMAIN from homeassistant.components.green_planet_energy.services import ( @@ -158,7 +158,7 @@ async def test_get_prices_non_quarter_hour_rejected( init_integration: MockConfigEntry, ) -> None: """Hours must be a multiple of 0.25 according to schema validation.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await _call_get_prices(hass, 0.3, init_integration.entry_id) diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 4bbe7067dadf15..a5302f62ffc15d 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -7,9 +7,9 @@ import growattServer from growattServer import GrowattV1ApiErrorCode +import probatio import pytest import requests -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.growatt_server.const import ( @@ -791,7 +791,7 @@ async def test_reauth_password_success( region_key = next( k for k in result["data_schema"].schema - if isinstance(k, vol.Required) and k.schema == CONF_REGION + if isinstance(k, probatio.Required) and k.schema == CONF_REGION ) assert region_key.default() == expected_region @@ -1180,7 +1180,7 @@ async def test_reconfigure_password_success( region_key = next( k for k in result["data_schema"].schema - if isinstance(k, vol.Required) and k.schema == CONF_REGION + if isinstance(k, probatio.Required) and k.schema == CONF_REGION ) assert region_key.default() == expected_region diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 858336aedd53f6..933df318454cda 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -29,8 +29,8 @@ SupervisorOptions, ) from freezegun.api import FrozenDateTimeFactory +from probatio import Invalid import pytest -from voluptuous import Invalid from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend, hassio diff --git a/tests/components/hdmi_cec/test_init.py b/tests/components/hdmi_cec/test_init.py index b4b8ff888257b7..e2d1f8307f9ba4 100644 --- a/tests/components/hdmi_cec/test_init.py +++ b/tests/components/hdmi_cec/test_init.py @@ -5,8 +5,8 @@ from typing import Any from unittest.mock import ANY, MagicMock, PropertyMock, call, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.hdmi_cec import ( DOMAIN, @@ -301,7 +301,7 @@ async def test_service_update_devices( "While the code allows for an empty string" " the schema doesn't allow it" ), - raises=vol.MultipleInvalid, + raises=probatio.MultipleInvalid, ), ), ], @@ -394,7 +394,7 @@ async def test_service_volume_release( " pass an empty string, but the schema" " does not allow this" ), - raises=vol.MultipleInvalid, + raises=probatio.MultipleInvalid, ), ), ], @@ -452,7 +452,7 @@ async def test_service_volume_mute( "`att` only accepts a int or a HEX value," " it seems good to allow for raw data here." ), - raises=vol.MultipleInvalid, + raises=probatio.MultipleInvalid, ), ), pytest.param( @@ -480,7 +480,7 @@ async def test_service_volume_mute( " passthrough a list, the call schema does" " not allow it." ), - raises=(vol.MultipleInvalid, TypeError), + raises=(probatio.MultipleInvalid, TypeError), ), ), ], diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index f5144ee86e57bb..b73a71f4c9ac78 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -4,8 +4,8 @@ from unittest.mock import patch from freezegun import freeze_time +import probatio import pytest -import voluptuous as vol from homeassistant import config as hass_config, core as ha from homeassistant.components.history_stats.const import ( @@ -136,7 +136,7 @@ async def test_setup_multiple_states( def test_setup_invalid_config(config) -> None: """Test the history statistics sensor setup with invalid config.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): SENSOR_SCHEMA(config) diff --git a/tests/components/home_connect/test_services.py b/tests/components/home_connect/test_services.py index 4eb54639c471e4..56187952793825 100644 --- a/tests/components/home_connect/test_services.py +++ b/tests/components/home_connect/test_services.py @@ -15,9 +15,9 @@ ) from aiohomeconnect.model.error import HomeConnectError, NoProgramActiveError from aiohomeconnect.model.program import ProgramDefinitionOption +from probatio.error import MultipleInvalid import pytest from syrupy.assertion import SnapshotAssertion -from voluptuous.error import MultipleInvalid from homeassistant.components import home_connect from homeassistant.components.home_connect.const import ( diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index c2e612dfd78477..3b35c2029b34af 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import Mock, patch +import probatio import pytest -import voluptuous as vol import yaml from homeassistant import config, core as ha @@ -338,7 +338,7 @@ async def test_turn_on_off_toggle_schema( for service in SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE: for invalid in None, "nothing", ENTITY_MATCH_ALL, ENTITY_MATCH_NONE: - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( ha.DOMAIN, service, diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index c5d5bc66fd4e46..f135230884263b 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol import yaml from homeassistant import config @@ -322,7 +322,7 @@ async def test_ensure_no_intersection(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "scene", {"scene": {}}) await hass.async_block_till_done() - with pytest.raises(vol.MultipleInvalid) as ex: + with pytest.raises(probatio.MultipleInvalid) as ex: await hass.services.async_call( "scene", "create", diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 73ef9efe48d42a..ad8437b141a333 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -300,7 +300,7 @@ async def test_if_fires_on_event_with_nested_data( ) -> None: """Test the firing of events with nested data. - This test exercises the slow path of using vol.Schema to validate + This test exercises the slow path of using probatio.Schema to validate matching event data. """ assert await async_setup_component( @@ -463,7 +463,7 @@ async def test_if_fires_on_multiple_user_ids( ) -> None: """Test the firing of event when the trigger has multiple user ids. - This test exercises the slow path of using vol.Schema to validate + This test exercises the slow path of using probatio.Schema to validate matching event context. """ assert await async_setup_component( diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index bc2a02f593d67b..075e98b3dfb72b 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -5,8 +5,8 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import ( @@ -1819,7 +1819,7 @@ async def test_if_fires_on_entities_change_overlap_for_template( async def test_below_above(hass: HomeAssistant) -> None: """Test above cannot be above below.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await numeric_state_trigger.async_validate_trigger_config( hass, {"platform": "numeric_state", "above": 1200, "below": 1000} ) @@ -1827,7 +1827,7 @@ async def test_below_above(hass: HomeAssistant) -> None: async def test_schema_unacceptable_entities(hass: HomeAssistant) -> None: """Test input_number, number & sensor only is accepted for above/below.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await numeric_state_trigger.async_validate_trigger_config( hass, { @@ -1836,7 +1836,7 @@ async def test_schema_unacceptable_entities(hass: HomeAssistant) -> None: "below": 1000, }, ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await numeric_state_trigger.async_validate_trigger_config( hass, { diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 5ab5b34a6f9163..429fa5879deefb 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -4,8 +4,8 @@ from unittest.mock import Mock, patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import time @@ -810,7 +810,7 @@ def test_schema_valid(conf) -> None: ) def test_schema_invalid(conf) -> None: """Make sure we don't accept number for 'at' value.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): time.TRIGGER_SCHEMA(conf) @@ -1145,7 +1145,7 @@ def test_weekday_validation() -> None: # Invalid weekday invalid_config = {"platform": "time", "at": "5:00:00", "weekday": "invalid"} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): time.TRIGGER_SCHEMA(invalid_config) # Invalid weekday in list @@ -1154,5 +1154,5 @@ def test_weekday_validation() -> None: "at": "5:00:00", "weekday": ["mon", "invalid"], } - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): time.TRIGGER_SCHEMA(invalid_config) diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index 2e7fa9dae08487..a7e78762bad551 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -3,8 +3,8 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components import automation from homeassistant.components.homeassistant.triggers import time_pattern @@ -370,5 +370,5 @@ async def test_invalid_schemas() -> None: ) for value in schemas: - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): time_pattern.TRIGGER_SCHEMA(value) diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index ac9d2642971460..a46547870e201a 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -200,7 +200,7 @@ def mock_multiprotocol_platform( def get_suggested(schema, key): - """Get suggested value for key in voluptuous schema.""" + """Get suggested value for key in probatio schema.""" for k in schema: if k == key: if k.description is None or "suggested_value" not in k.description: diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 1a3541ed6a1de7..d3fa5699bebadf 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -3,8 +3,8 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.homekit.accessories import HomeDriver @@ -1435,7 +1435,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( # sonos_config_switch.entity_id is a config category entity # so it should not be selectable since it will always be excluded - with pytest.raises(vol.error.Invalid): + with pytest.raises(probatio.error.Invalid): await hass.config_entries.options.async_configure( result2["flow_id"], user_input={"entities": [sonos_config_switch.entity_id]}, @@ -1534,7 +1534,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( # sonos_hidden_switch.entity_id is a hidden entity # so it should not be selectable since it will always be excluded - with pytest.raises(vol.error.Invalid): + with pytest.raises(probatio.error.Invalid): await hass.config_entries.options.async_configure( result2["flow_id"], user_input={"entities": [sonos_hidden_switch.entity_id]}, diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index ac58a798751516..b17f5ea147908a 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock, Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.homekit.const import ( BRIDGE_NAME, @@ -164,7 +164,7 @@ def test_validate_entity_config() -> None: ] for conf in configs: - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): vec(conf) assert vec({}) == {} diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index b415e54af0479c..d2923b937e38ac 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -4,7 +4,7 @@ from unittest.mock import Mock from aiohttp import web -import voluptuous as vol +import probatio from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator @@ -36,7 +36,8 @@ async def post(self, request, data): async def test_validator(aiohttp_client: ClientSessionGenerator) -> None: """Test the validator.""" client = await get_client( - aiohttp_client, RequestDataValidator(vol.Schema({vol.Required("test"): str})) + aiohttp_client, + RequestDataValidator(probatio.Schema({probatio.Required("test"): str})), ) resp = await client.post("/", json={"test": "bla"}) @@ -54,11 +55,11 @@ async def test_validator_allow_empty(aiohttp_client: ClientSessionGenerator) -> client = await get_client( aiohttp_client, RequestDataValidator( - vol.Schema( + probatio.Schema( { # Although we allow empty, our schema should still be able # to validate an empty dict. - vol.Optional("test"): str + probatio.Optional("test"): str } ), allow_empty=True, diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index ae017a3602134a..59ca1b665551b7 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -13,8 +13,8 @@ HTTPInternalServerError, HTTPUnauthorized, ) +import probatio import pytest -import voluptuous as vol from homeassistant.components.http import KEY_HASS from homeassistant.components.http.request_context import current_request @@ -72,7 +72,7 @@ async def test_handling_invalid_data(mock_request: Mock) -> None: await request_handler_factory( mock_request.app[KEY_HASS], Mock(requires_auth=False), - AsyncMock(side_effect=vol.Invalid("yo")), + AsyncMock(side_effect=probatio.Invalid("yo")), )(mock_request) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 5619a99985aea1..9784e325fd48f6 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -5,8 +5,8 @@ from aiohue.discovery import URL_NUPNP from aiohue.errors import AiohueException, LinkButtonNotPressed +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.hue import DOMAIN, config_flow, const @@ -229,7 +229,7 @@ async def test_flow_bridges_discovered( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): assert result["data_schema"]({"id": "not-discovered"}) result["data_schema"]({"id": "bla"}) @@ -254,7 +254,7 @@ async def test_flow_two_bridges_discovered_one_new( assert result["step_id"] == "init" assert result["data_schema"]({"id": "beer"}) assert result["data_schema"]({"id": "manual"}) - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): assert not result["data_schema"]({"id": "bla"}) diff --git a/tests/components/humidifier/test_condition.py b/tests/components/humidifier/test_condition.py index 560ea0525174b5..2b034e0a69f0d9 100644 --- a/tests/components/humidifier/test_condition.py +++ b/tests/components/humidifier/test_condition.py @@ -3,8 +3,8 @@ from contextlib import AbstractContextManager, nullcontext as does_not_raise from typing import Any +import probatio import pytest -import voluptuous as vol from homeassistant.components.humidifier.condition import CONDITIONS from homeassistant.components.humidifier.const import ( @@ -381,13 +381,13 @@ async def test_humidifier_numerical_condition_behavior_all( "humidifier.is_mode", # Empty mode list {CONF_MODE: []}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( "humidifier.is_mode", # Missing CONF_MODE {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) diff --git a/tests/components/humidifier/test_trigger.py b/tests/components/humidifier/test_trigger.py index 9a21651c3b1b13..d946949c47e6f2 100644 --- a/tests/components/humidifier/test_trigger.py +++ b/tests/components/humidifier/test_trigger.py @@ -3,8 +3,8 @@ from contextlib import AbstractContextManager, nullcontext as does_not_raise from typing import Any +import probatio import pytest -import voluptuous as vol from homeassistant.components.humidifier.const import ( ATTR_ACTION, @@ -402,13 +402,13 @@ async def test_humidifier_state_attribute_trigger_behavior_all( "humidifier.mode_changed", # Empty mode list {CONF_MODE: []}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( "humidifier.mode_changed", # Missing CONF_MODE {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 4b816d479696a7..5d5d05c0be8443 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -6,8 +6,8 @@ from aioautomower.exceptions import ApiError from aioautomower.model import MowerActivities, MowerAttributes, MowerStates from freezegun.api import FrozenDateTimeFactory +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components.husqvarna_automower.const import DOMAIN from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL diff --git a/tests/components/idrive_e2/test_config_flow.py b/tests/components/idrive_e2/test_config_flow.py index 0e524decc25cb5..e184150f893b99 100644 --- a/tests/components/idrive_e2/test_config_flow.py +++ b/tests/components/idrive_e2/test_config_flow.py @@ -5,8 +5,8 @@ from botocore.exceptions import EndpointConnectionError from idrive_e2 import CannotConnect, InvalidAuth +import probatio import pytest -import voluptuous as vol from homeassistant.components.idrive_e2 import ClientError from homeassistant.components.idrive_e2.config_flow import CONF_ACCESS_KEY_ID @@ -231,7 +231,7 @@ async def test_flow_bucket_step_options_from_s3_list_buckets( # Extract dropdown options from selector in schema schema = result["data_schema"].schema - selector = schema[vol.Required(CONF_BUCKET)] + selector = schema[probatio.Required(CONF_BUCKET)] assert isinstance(selector, SelectSelector) cfg = selector.config diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index f8560bc2e0019c..3b96a19d059e5f 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, patch from aioimaplib import AioImapException +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.imap.const import ( @@ -448,7 +448,7 @@ async def test_options_flow_when_connection_fails( # Check if entry was updated for key, value in new_config.items(): assert entry.data[key] == value - except vol.Invalid: + except probatio.Invalid: # Check if form was expected with these options assert assert_result is FlowResultType.FORM diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 4a8f9ce52da3bb..e56720b5f8418c 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -8,8 +8,8 @@ from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError from influxdb_client.rest import ApiException +from probatio import Invalid import pytest -from voluptuous import Invalid from homeassistant.components import sensor from homeassistant.components.influxdb.const import ( diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 9c1dad0757f2bd..1648b187f1e904 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -5,8 +5,8 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components.input_datetime import ( ATTR_DATE, @@ -130,7 +130,7 @@ async def async_set_timestamp( ) def test_invalid_configs(config) -> None: """Test config.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): CONFIG_SCHEMA({DOMAIN: config}) @@ -284,7 +284,7 @@ async def test_set_invalid(hass: HomeAssistant) -> None: dt_obj = datetime.datetime(2017, 9, 7, 19, 46) time_portion = dt_obj.time() - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "set_datetime", @@ -314,7 +314,7 @@ async def test_set_invalid_2(hass: HomeAssistant) -> None: dt_obj = datetime.datetime(2017, 9, 7, 19, 46) time_portion = dt_obj.time() - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "set_datetime", diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index f5cf05e797adf5..1c1f745cc9193d 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -3,8 +3,8 @@ from typing import Any from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.input_number import ( ATTR_VALUE, @@ -134,7 +134,7 @@ async def test_set_value(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) state = hass.states.get(entity_id) assert float(state.state) == 70 - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: await set_value(hass, entity_id, "110") assert "Invalid value for input_number.test_1: 110.0 (range 0.0 - 100.0)" in str( diff --git a/tests/components/izone/test_climate.py b/tests/components/izone/test_climate.py index 74bf8f992320d1..d48e9d51eaa100 100644 --- a/tests/components/izone/test_climate.py +++ b/tests/components/izone/test_climate.py @@ -5,9 +5,9 @@ from freezegun.api import FrozenDateTimeFactory from pizone import Controller, ControllerCommandError, Zone +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, @@ -648,7 +648,7 @@ async def test_airflow_rejects_non_multiples_of_five( airflow: float, ) -> None: """Airflow services reject values that are not multiples of 5.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, service, diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index cb61c674051b72..2f051f0e2ae5bc 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock, patch +from probatio.error import Invalid import pytest -from voluptuous.error import Invalid from homeassistant import config_entries from homeassistant.components.jellyfin.client_wrapper import CannotConnect, InvalidAuth diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 0b065416cb9da9..7826aade744083 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -5,8 +5,8 @@ from ndms2_client import ConnectionException from ndms2_client.client import InterfaceInfo, RouterInfo +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.keenetic_ndms2 import CONF_INTERFACES, const @@ -239,7 +239,7 @@ async def test_options_interface_filter(hass: HomeAssistant) -> None: for i, s in result["data_schema"].schema.items() if i.schema == CONF_INTERFACES ) - assert isinstance(interfaces_schema, vol.Required) + assert isinstance(interfaces_schema, probatio.Required) assert interfaces_schema.default() == [] diff --git a/tests/components/kiosker/test_services.py b/tests/components/kiosker/test_services.py index 004ba5acd3f0a9..961fc17fd15eb4 100644 --- a/tests/components/kiosker/test_services.py +++ b/tests/components/kiosker/test_services.py @@ -12,9 +12,9 @@ ScreensaverState, TLSVerificationError, ) +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.kiosker.const import ( ATTR_BACKGROUND, @@ -197,7 +197,7 @@ async def test_schema_rejects_invalid_input( ) assert device is not None - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, service, diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py index ee7cae4bae698f..95bdab2af31011 100644 --- a/tests/components/kitchen_sink/test_init.py +++ b/tests/components/kitchen_sink/test_init.py @@ -4,9 +4,9 @@ from http import HTTPStatus from unittest.mock import ANY +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.components.labs import EVENT_LABS_UPDATED @@ -359,7 +359,7 @@ async def test_service( """Test we can call the service.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): await hass.services.async_call(DOMAIN, "test_service_1", blocking=True) await hass.services.async_call( diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index 2f1aa1e8c0a08e..d6e8fa50490324 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -70,7 +70,7 @@ async def test_diagnostic_config_error( # change this with every library bump knx.xknx.version = "0.0.0" # the snapshot will contain 'configuration_error' key with - # the voluptuous error message + # the probatio error message assert ( await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) == snapshot diff --git a/tests/components/labs/test_websocket_api.py b/tests/components/labs/test_websocket_api.py index 37749de09491ee..c6b82977058ee1 100644 --- a/tests/components/labs/test_websocket_api.py +++ b/tests/components/labs/test_websocket_api.py @@ -282,7 +282,7 @@ async def test_websocket_update_validates_enabled_parameter( msg = await client.receive_json() assert not msg["success"] - # Validation error from voluptuous + # Validation error from probatio async def test_storage_persists_preview_feature_across_calls( diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index e664e7293e8108..05fa22fa406725 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -3,8 +3,8 @@ from typing import Any from unittest.mock import MagicMock, mock_open, patch +import probatio import pytest -import voluptuous as vol from homeassistant import core from homeassistant.components import light @@ -391,14 +391,14 @@ async def test_services( {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: -1}, blocking=True, ) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_XY_COLOR: ["bla-di-bla", 5]}, blocking=True, ) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, @@ -416,7 +416,7 @@ async def test_services( assert data == {} # faulty attributes will not trigger a service call - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, @@ -427,7 +427,7 @@ async def test_services( }, blocking=True, ) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, @@ -1731,7 +1731,7 @@ async def test_light_turn_on_rgb_color_is_plain_tuple( Covers two input paths that both resolve to the same RGB value (128, 0, 0): - color_name: goes through color_name_to_rgb (returns RGBColor NamedTuple), - bypassing the service schema vol.Coerce(tuple) coercion. + bypassing the service schema probatio.Coerce(tuple) coercion. - rgb_color: RGBColor NamedTuple passed directly, converted by the schema. """ entities = [ @@ -2288,16 +2288,16 @@ def test_valid_supported_color_modes() -> None: # Supported color modes must not be empty supported = set() - with pytest.raises(vol.Error): + with pytest.raises(probatio.Error): light.valid_supported_color_modes(supported) # ColorMode.WHITE must be combined with a color mode supporting color supported = {light.ColorMode.WHITE} - with pytest.raises(vol.Error): + with pytest.raises(probatio.Error): light.valid_supported_color_modes(supported) supported = {light.ColorMode.WHITE, light.ColorMode.COLOR_TEMP} - with pytest.raises(vol.Error): + with pytest.raises(probatio.Error): light.valid_supported_color_modes(supported) supported = {light.ColorMode.WHITE, light.ColorMode.HS} @@ -2308,7 +2308,7 @@ def test_valid_supported_color_modes() -> None: assert light.valid_supported_color_modes(supported) == supported supported = {light.ColorMode.ONOFF, light.ColorMode.COLOR_TEMP} - with pytest.raises(vol.Error): + with pytest.raises(probatio.Error): light.valid_supported_color_modes(supported) # ColorMode.BRIGHTNESS must be the only supported mode @@ -2316,7 +2316,7 @@ def test_valid_supported_color_modes() -> None: assert light.valid_supported_color_modes(supported) == supported supported = {light.ColorMode.BRIGHTNESS, light.ColorMode.COLOR_TEMP} - with pytest.raises(vol.Error): + with pytest.raises(probatio.Error): light.valid_supported_color_modes(supported) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index a35d28c90e46ec..8eba24a06dc374 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -7,8 +7,8 @@ from unittest.mock import Mock from freezegun import freeze_time +import probatio import pytest -import voluptuous as vol from homeassistant import core as ha from homeassistant.components import logbook, recorder @@ -171,7 +171,7 @@ async def test_service_call_create_log_book_entry_no_message( """Test if service call create log book entry without message.""" calls = async_capture_events(hass_, logbook.EVENT_LOGBOOK_ENTRY) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass_.services.async_call(logbook.DOMAIN, "log", {}, True) # Logbook entry service call results in firing an event. diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 34fe1868b0011e..443fed6d6af805 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -4,8 +4,8 @@ from typing import Any from unittest.mock import MagicMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.lovelace import DOMAIN, _validate_url_slug from homeassistant.core import HomeAssistant @@ -123,5 +123,5 @@ def test_validate_url_slug_valid(value: str, expected: str) -> None: ) def test_validate_url_slug_invalid(value: Any, error_message: str) -> None: """Test _validate_url_slug with invalid values.""" - with pytest.raises(vol.Invalid, match=error_message): + with pytest.raises(probatio.Invalid, match=error_message): _validate_url_slug(value) diff --git a/tests/components/lutron/test_config_flow.py b/tests/components/lutron/test_config_flow.py index fe52285eaed44b..2d34240707f509 100644 --- a/tests/components/lutron/test_config_flow.py +++ b/tests/components/lutron/test_config_flow.py @@ -164,7 +164,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: # Try to set an out of range dimmer level (260) out_of_range_level = 260 - # The voluptuous validation will raise an exception before the handler processes it + # The probatio validation will raise an exception before the handler processes it with pytest.raises(InvalidData): await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/mcp/test_init.py b/tests/components/mcp/test_init.py index 745d47870f9254..a25005da434504 100644 --- a/tests/components/mcp/test_init.py +++ b/tests/components/mcp/test_init.py @@ -7,8 +7,8 @@ import httpx from mcp import McpError from mcp.types import CallToolResult, ErrorData, ListToolsResult, TextContent, Tool +import probatio import pytest -import voluptuous as vol from homeassistant.components.mcp.const import CONF_SLUG, DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -300,7 +300,7 @@ async def test_llm_get_api_tools( assert tool.name == "search_memory" assert tool.description == "Search memory for relevant context based on a query." with pytest.raises( - vol.Invalid, match=re.escape("required key not provided at 'query'") + probatio.Invalid, match=re.escape("required key not provided at 'query'") ): tool.parameters({}) assert tool.parameters({"query": "frogs"}) == {"query": "frogs"} @@ -309,7 +309,7 @@ async def test_llm_get_api_tools( assert tool.name == "save_memory" assert tool.description == "Save a memory context." with pytest.raises( - vol.Invalid, match=re.escape("required key not provided at 'context'") + probatio.Invalid, match=re.escape("required key not provided at 'context'") ): tool.parameters({}) assert tool.parameters({"context": {"fact": "User was born in February"}}) == { diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index c8945413ba6966..da6bc0e95fde1d 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -4,8 +4,8 @@ from unittest.mock import patch from aiohttp import hdrs +import probatio import pytest -import voluptuous as vol from homeassistant.components import media_player from homeassistant.components.media_player import ( @@ -738,7 +738,7 @@ async def test_enqueue_alert_exclusive(hass: HomeAssistant) -> None: await async_setup_component(hass, DOMAIN, {"media_player": {"platform": "demo"}}) await hass.async_block_till_done() - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( "media_player", "play_media", @@ -828,7 +828,7 @@ async def test_play_media_via_selector(hass: HomeAssistant) -> None: assert len(mock_play_media.mock_calls) == 2 assert mock_play_media.mock_calls[0].args == mock_play_media.mock_calls[1].args - with pytest.raises(vol.Invalid, match="Play media cannot contain 'media'"): + with pytest.raises(probatio.Invalid, match="Play media cannot contain 'media'"): await hass.services.async_call( "media_player", "play_media", diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py index b65ec9610d5794..e432c028819838 100644 --- a/tests/components/melnor/test_config_flow.py +++ b/tests/components/melnor/test_config_flow.py @@ -2,8 +2,8 @@ from unittest.mock import AsyncMock +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.melnor.const import DOMAIN @@ -52,7 +52,7 @@ async def test_user_step_discovered_devices( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pick_device" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADDRESS: "wrong_address"} ) @@ -99,7 +99,7 @@ async def test_user_step_with_existing_device( assert result["type"] is FlowResultType.FORM - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADDRESS: FAKE_ADDRESS_1} ) diff --git a/tests/components/miele/test_services.py b/tests/components/miele/test_services.py index 9bafe8ff5754b4..ccab4a31c4f9a2 100644 --- a/tests/components/miele/test_services.py +++ b/tests/components/miele/test_services.py @@ -4,9 +4,9 @@ from unittest.mock import MagicMock, Mock from aiohttp import ClientResponseError +from probatio import MultipleInvalid import pytest from syrupy.assertion import SnapshotAssertion -from voluptuous import MultipleInvalid from homeassistant.components.miele.const import DOMAIN from homeassistant.components.miele.services import ( diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index c10b4c98cc9565..4c63da7a731303 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -18,10 +18,10 @@ from unittest import mock from freezegun.api import FrozenDateTimeFactory +import probatio from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse import pytest -import voluptuous as vol from homeassistant import config as hass_config from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -156,10 +156,10 @@ async def test_fixedRegList_validator() -> None: ): assert isinstance(hvac_fixedsize_reglist_validator(value), list) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): hvac_fixedsize_reglist_validator([15, "ab", 17, 18, 19, 20, 21]) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): hvac_fixedsize_reglist_validator([15, 17]) @@ -171,13 +171,13 @@ async def test_register_int_list_validator() -> None: ): assert isinstance(register_int_list_validator(value), vtype) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): register_int_list_validator([15, 16]) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): register_int_list_validator(-15) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): register_int_list_validator(["aq"]) @@ -192,9 +192,9 @@ async def test_nan_validator() -> None: ): assert isinstance(nan_validator(value), value_type) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): nan_validator("x15") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): nan_validator("not a hex string") @@ -251,7 +251,7 @@ async def test_ok_struct_validator(do_config) -> None: """Test struct validator.""" try: struct_validator(do_config) - except vol.Invalid: + except probatio.Invalid: pytest.fail("struct_validator unexpected exception") @@ -346,7 +346,7 @@ async def test_exception_struct_validator(do_config) -> None: """Test struct validator.""" try: struct_validator(do_config) - except vol.Invalid: + except probatio.Invalid: return pytest.fail("struct_validator missing exception") @@ -1623,12 +1623,12 @@ async def test_pb_service_write_no_slave( async def test_ensure_and_check_conflicting_scales_and_offsets(do_config) -> None: """Test ensure_and_check_conflicting_scales_and_offsets.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ensure_and_check_conflicting_scales_and_offsets(do_config[0]) async def test_not_zero_value() -> None: """Test not 0 validator validator.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): not_zero_value(0, "Value cannot be zero.") diff --git a/tests/components/monzo/test_services.py b/tests/components/monzo/test_services.py index ca9650ee6d02d3..07e33f4fa6cff6 100644 --- a/tests/components/monzo/test_services.py +++ b/tests/components/monzo/test_services.py @@ -8,8 +8,8 @@ from aiohttp import ClientError from monzopy import AuthorisationExpiredError, InvalidMonzoAPIResponseError +import probatio import pytest -import voluptuous as vol from homeassistant.components.monzo.const import ( DEVICE_MODEL_ACCOUNT, @@ -175,7 +175,7 @@ async def test_invalid_amount( amount: object, ) -> None: """Test invalid transfer amounts are rejected by the action schema.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await _async_call_transfer( hass, transfer_devices, diff --git a/tests/components/moon/test_condition.py b/tests/components/moon/test_condition.py index 515c2ad4786449..3694bad4d00ef4 100644 --- a/tests/components/moon/test_condition.py +++ b/tests/components/moon/test_condition.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.helpers import condition @@ -77,7 +77,7 @@ async def test_conditions( async def test_is_phase_rejects_unknown_phase(hass: HomeAssistant) -> None: """Test that the is_phase condition rejects an unknown phase.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await condition.async_validate_condition_config( hass, {"condition": "moon.is_phase", "options": {"phase": "not_a_phase"}} ) diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py index d42e4d76856c38..dc8a1fff8d0c8d 100644 --- a/tests/components/mopeka/test_config_flow.py +++ b/tests/components/mopeka/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -import voluptuous as vol +import probatio from homeassistant import config_entries from homeassistant.components.mopeka.const import CONF_MEDIUM_TYPE, DOMAIN, MediumType @@ -256,7 +256,7 @@ async def test_async_step_reconfigure_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" - schema: vol.Schema = result["data_schema"] + schema: probatio.Schema = result["data_schema"] medium_type_key = next( iter(key for key in schema.schema if key == CONF_MEDIUM_TYPE) ) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 0932756e6a7a15..4a5fe919c7da79 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -24,8 +24,8 @@ KEY_TEXT_OVERLAY_TIMESTAMP, KEY_VIDEO_STREAMING, ) +import probatio import pytest -import voluptuous as vol from homeassistant.components.camera import async_get_image, async_get_mjpeg_stream from homeassistant.components.motioneye import get_motioneye_device_identifier @@ -412,7 +412,7 @@ async def test_set_text_overlay_bad_extra_key(hass: HomeAssistant) -> None: await setup_mock_motioneye_config_entry(hass, client=client) data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID, "extra_key": "foo"} - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) @@ -427,7 +427,7 @@ async def test_set_text_overlay_bad_entity_identifier(hass: HomeAssistant) -> No } client.reset_mock() - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) @@ -435,7 +435,7 @@ async def test_set_text_overlay_bad_empty(hass: HomeAssistant) -> None: """Test text overlay with incorrect input data.""" client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, {}) @@ -445,7 +445,7 @@ async def test_set_text_overlay_bad_no_left_or_right(hass: HomeAssistant) -> Non await setup_mock_motioneye_config_entry(hass, client=client) data = {ATTR_ENTITY_ID: TEST_CAMERA_ENTITY_ID} - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): await hass.services.async_call(DOMAIN, SERVICE_SET_TEXT_OVERLAY, data) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 068926c073fb59..f9aadfcb449be3 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -9,8 +9,8 @@ from unittest.mock import ANY, MagicMock, patch from freezegun import freeze_time +import probatio import pytest -import voluptuous as vol import yaml from homeassistant import config as module_hass_config @@ -1290,7 +1290,7 @@ async def help_test_default_availability_list_single( with ( patch("homeassistant.config.load_yaml_config_file", return_value=config), - suppress(vol.MultipleInvalid), + suppress(probatio.MultipleInvalid), ): await mqtt_mock_entry() diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index e5093bf872b13f..d5e9d5c1f1d82b 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -5,8 +5,8 @@ from typing import Any from unittest.mock import call, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import climate from homeassistant.components.climate import ( @@ -215,7 +215,7 @@ async def test_set_operation_bad_attr_and_state( state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE) # type:ignore[arg-type] assert ( "expected HVACMode or one of 'off', 'heat', 'cool', 'heat_cool', 'auto', 'dry'," @@ -483,7 +483,7 @@ async def test_set_fan_mode_bad_attr( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: await common.async_set_fan_mode(hass, None, ENTITY_CLIMATE) # type:ignore[arg-type] assert "string value is None at 'fan_mode'" in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) @@ -578,14 +578,14 @@ async def test_set_swing_mode_bad_attr( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: await common.async_set_swing_mode(hass, None, ENTITY_CLIMATE) # type:ignore[arg-type] assert "string value is None at 'swing_mode'" in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" assert state.attributes.get("swing_horizontal_mode") == "off" - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: await common.async_set_swing_horizontal_mode(hass, None, ENTITY_CLIMATE) # type:ignore[arg-type] assert "string value is None at 'swing_horizontal_mode'" in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 5096957770eb4c..89c85f930af82a 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -11,8 +11,8 @@ from aiohasupervisor import SupervisorError from aiohasupervisor.models import Discovery +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components import mqtt @@ -1294,7 +1294,7 @@ async def test_keepalive_validation( assert result["step_id"] == "broker" if error: - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=test_input, @@ -1429,11 +1429,11 @@ async def test_invalid_discovery_prefix( assert mock_reload_after_entry_update.call_count == 0 -def get_default(schema: vol.Schema, key: str) -> Any | None: - """Get default value for key in voluptuous schema.""" +def get_default(schema: probatio.Schema, key: str) -> Any | None: + """Get default value for key in probatio schema.""" for schema_key in schema: # type:ignore[attr-defined] if schema_key == key: - if schema_key.default == vol.UNDEFINED: + if schema_key.default == probatio.UNDEFINED: return None return schema_key.default() return None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 0041ab22fe3b4e..af7d7c9d40d240 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -4,8 +4,8 @@ from typing import Any from unittest.mock import patch +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components import fan from homeassistant.components.fan import ( diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 3d35ddfe9e62b2..a2c4f6d182663d 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -4,8 +4,8 @@ from typing import Any from unittest.mock import patch +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components import humidifier from homeassistant.components.humidifier import ( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 20e035b7b3b1ba..f0190c69773c1f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -12,8 +12,8 @@ from freezegun.api import FrozenDateTimeFactory from paho.mqtt.client import MQTTMessage, Properties +import probatio import pytest -import voluptuous as vol from homeassistant import core as ha from homeassistant.components import mqtt @@ -271,7 +271,7 @@ async def test_service_call_without_topic_does_not_publish( ) -> None: """Test the service call if topic is missing.""" mqtt_mock = await mqtt_mock_entry() - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, mqtt.SERVICE_PUBLISH, @@ -311,7 +311,7 @@ async def test_service_call_with_template_topic_renders_invalid_topic( If a wildcard topic is rendered, then fail. """ mqtt_mock = await mqtt_mock_entry() - with pytest.raises(vol.Invalid) as exc: + with pytest.raises(probatio.Invalid) as exc: await hass.services.async_call( DOMAIN, mqtt.SERVICE_PUBLISH, @@ -528,44 +528,44 @@ async def test_publish_api_with_falback_to_none( def test_validate_topic() -> None: """Test topic name/filter validation.""" # Invalid UTF-8, must not contain U+D800 to U+DFFF. - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\ud800") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\udfff") # Topic MUST NOT be empty - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("") # Topic MUST NOT be longer than 65535 encoded bytes. - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("ü" * 32768) # UTF-8 MUST NOT include null character - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("bad\0one") # Topics "SHOULD NOT" include these special characters # (not MUST NOT, RFC2119). The receiver MAY close the connection. # We enforce this because mosquitto does: https://github.com/eclipse/mosquitto/commit/94fdc9cb44c829ff79c74e1daa6f7d04283dfffd - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\u0001") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\u001f") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\u007f") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\u009f") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\ufdd0") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\ufdef") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\ufffe") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\ufffe") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\uffff") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\U0001fffe") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.util.valid_topic("\U0001ffff") @@ -573,24 +573,24 @@ def test_validate_subscribe_topic() -> None: """Test invalid subscribe topics.""" mqtt.valid_subscribe_topic("#") mqtt.valid_subscribe_topic("sport/#") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("sport/#/") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("foo/bar#") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("foo/#/bar") mqtt.valid_subscribe_topic("+") mqtt.valid_subscribe_topic("+/tennis/#") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("sport+") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("sport+/") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("sport/+1") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("sport/+#") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_subscribe_topic("bad+topic") mqtt.valid_subscribe_topic("sport/+/player1") mqtt.valid_subscribe_topic("/finance") @@ -600,13 +600,13 @@ def test_validate_subscribe_topic() -> None: def test_validate_publish_topic() -> None: """Test invalid publish topics.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_publish_topic("pub+") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_publish_topic("pub/+") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_publish_topic("1#") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): mqtt.valid_publish_topic("bad+topic") mqtt.valid_publish_topic("//") @@ -657,7 +657,7 @@ def test_entity_device_info_schema() -> None: } ) # no identifiers - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "manufacturer": "Whatever", @@ -667,13 +667,13 @@ def test_entity_device_info_schema() -> None: } ) # empty identifiers - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): MQTT_ENTITY_DEVICE_INFO_SCHEMA( {"identifiers": [], "connections": [], "name": "Beer"} ) # not a valid URL - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "manufacturer": "Whatever", diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index c5b222c32795c2..e011c05bb32890 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -5,8 +5,8 @@ from typing import Any from unittest.mock import call, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import water_heater from homeassistant.components.mqtt.const import DOMAIN @@ -167,7 +167,7 @@ async def test_set_operation_mode_bad_attr_and_state( state = hass.states.get(ENTITY_WATER_HEATER) assert state.state == "off" - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: await common.async_set_operation_mode(hass, None, ENTITY_WATER_HEATER) # type:ignore[arg-type] assert "string value is None at 'operation_mode'" in str(excinfo.value) state = hass.states.get(ENTITY_WATER_HEATER) diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index 3557f6f516a539..3f7b4a03b2dd01 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -12,10 +12,10 @@ from music_assistant_models.errors import UserNotFoundError from music_assistant_models.media_items import Track from music_assistant_models.player import PlayerMedia +import probatio import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import paths -import voluptuous as vol from homeassistant.components.media_player import ( ATTR_GROUP_MEMBERS, @@ -1128,7 +1128,7 @@ async def test_media_player_play_announcement_action_invalid_input( ) -> None: """Test play_announcement action requires either a url or a message with an entity.""" await setup_integration_from_fixtures(hass, music_assistant_client) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_PLAY_ANNOUNCEMENT, diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py index 5a840f76bb2cd8..2954a93881356f 100644 --- a/tests/components/mysensors/test_gateway.py +++ b/tests/components/mysensors/test_gateway.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.mysensors.gateway import is_serial_port from homeassistant.core import HomeAssistant @@ -27,7 +27,7 @@ def test_is_serial_port_windows( with patch("sys.platform", "win32"): try: is_serial_port(port) - except vol.Invalid: + except probatio.Invalid: assert not expect_valid else: assert expect_valid diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index e9f93ef5ad8df4..b70a074d8b9d57 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -4,9 +4,9 @@ from typing import Any from unittest.mock import AsyncMock, patch +from probatio.error import MultipleInvalid import pytest from syrupy.assertion import SnapshotAssertion -from voluptuous.error import MultipleInvalid from homeassistant.components.climate import ( ATTR_HVAC_MODE, diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index f9f344aa4e9d3b..665874ceef11dd 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -3,8 +3,8 @@ import copy from unittest.mock import MagicMock +import probatio import pytest -import voluptuous as vol from homeassistant.components import notify from homeassistant.components.notify import ( @@ -125,7 +125,7 @@ async def test_send_message_service( entity.send_message_mock_calls.reset_mock() # Test schema: `None` message fails - with pytest.raises(vol.Invalid) as exc: + with pytest.raises(probatio.Invalid) as exc: await hass.services.async_call( notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, @@ -135,7 +135,7 @@ async def test_send_message_service( entity.send_message_mock_calls.assert_not_called() # Test schema: No message fails - with pytest.raises(vol.Invalid) as exc: + with pytest.raises(probatio.Invalid) as exc: await hass.services.async_call( notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, {"entity_id": "notify.test"} ) diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py index 977486ab768930..014ef8d44fd553 100644 --- a/tests/components/notify/test_legacy.py +++ b/tests/components/notify/test_legacy.py @@ -6,8 +6,8 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch +import probatio import pytest -import voluptuous as vol import yaml from homeassistant import config as hass_config @@ -552,7 +552,7 @@ async def async_get_service2( async def test_sending_none_message(hass: HomeAssistant, tmp_path: Path) -> None: """Test send with None as message.""" send_message_mock = await help_setup_notify(hass, tmp_path) - with pytest.raises(vol.Invalid) as exc: + with pytest.raises(probatio.Invalid) as exc: await hass.services.async_call( notify.DOMAIN, notify.SERVICE_NOTIFY, {notify.ATTR_MESSAGE: None} ) diff --git a/tests/components/ntfy/test_services.py b/tests/components/ntfy/test_services.py index a1e3e6b7d8053a..09eceac06717e4 100644 --- a/tests/components/ntfy/test_services.py +++ b/tests/components/ntfy/test_services.py @@ -8,8 +8,8 @@ NtfyHTTPError, NtfyUnauthorizedAuthenticationError, ) +import probatio import pytest -import voluptuous as vol from yarl import URL from homeassistant.components import camera, image, media_source @@ -205,7 +205,7 @@ async def test_send_message_exception( "Delayed email notifications are not supported", ), ( - vol.MultipleInvalid, + probatio.MultipleInvalid, { ATTR_ATTACH: "https://example.com/Epic Sax Guy 10 Hours.mp4", ATTR_ATTACH_FILE: { @@ -218,14 +218,14 @@ async def test_send_message_exception( "Only one attachment source is allowed: URL or local file", ), ( - vol.MultipleInvalid, + probatio.MultipleInvalid, { ATTR_FILENAME: "Epic Sax Guy 10 Hours.mp4", }, "Filename only allowed when attachment is provided", ), ( - vol.MultipleInvalid, + probatio.MultipleInvalid, { ATTR_ACTIONS: [ {"action": "broadcast", "label": "1"}, diff --git a/tests/components/obihai/__init__.py b/tests/components/obihai/__init__.py index 7b483514dcfc0f..a3051db8c8b1fe 100644 --- a/tests/components/obihai/__init__.py +++ b/tests/components/obihai/__init__.py @@ -26,7 +26,7 @@ def get_device_mac(self): def get_schema_suggestion(schema, key): - """Get suggested value for key in voluptuous schema.""" + """Get suggested value for key in probatio schema.""" for k in schema: if k == key: if k.description is None or "suggested_value" not in k.description: diff --git a/tests/components/ollama/test_ai_task.py b/tests/components/ollama/test_ai_task.py index c28b29ca97df98..3e898ea358fe81 100644 --- a/tests/components/ollama/test_ai_task.py +++ b/tests/components/ollama/test_ai_task.py @@ -4,8 +4,8 @@ from unittest.mock import patch import ollama +import probatio import pytest -import voluptuous as vol from homeassistant.components import ai_task, media_source from homeassistant.components.ollama import CONF_KEEP_ALIVE @@ -180,9 +180,9 @@ async def mock_chat_response(): task_name="Test Task", entity_id=entity_id, instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -236,9 +236,9 @@ async def mock_chat_response(): task_name="Test Task", entity_id=entity_id, instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index 8da87f02e20f51..0e8b589a94bb46 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -7,9 +7,9 @@ from freezegun.api import FrozenDateTimeFactory from ollama import Message, ResponseError +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import conversation, ollama from homeassistant.components.conversation import trace @@ -310,9 +310,9 @@ async def test_function_call( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str}, - extra=vol.ALLOW_EXTRA, + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str}, + extra=probatio.ALLOW_EXTRA, ) mock_tool.async_call.return_value = "Test response" @@ -395,8 +395,8 @@ async def test_function_exception( mock_tool = AsyncMock() mock_tool.name = "test_tool" mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} + mock_tool.parameters = probatio.Schema( + {probatio.Optional("param1", description="Test parameters"): str} ) mock_tool.async_call.side_effect = HomeAssistantError("Test tool exception") diff --git a/tests/components/onedrive/test_services.py b/tests/components/onedrive/test_services.py index f2f9d15c432da9..bcecee36735335 100644 --- a/tests/components/onedrive/test_services.py +++ b/tests/components/onedrive/test_services.py @@ -9,8 +9,8 @@ from unittest.mock import MagicMock, Mock, patch from onedrive_personal_sdk.exceptions import OneDriveException +import probatio import pytest -import voluptuous as vol from homeassistant.components.onedrive.const import CONF_DELETE_PERMANENTLY, DOMAIN from homeassistant.components.onedrive.services import ( @@ -706,7 +706,7 @@ async def test_delete_empty_destination_path( """Test delete service raises when destination_path is an empty list.""" await setup_integration(hass, mock_config_entry) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, DELETE_SERVICE, diff --git a/tests/components/open_router/test_ai_task.py b/tests/components/open_router/test_ai_task.py index dbfb4b146d7ab1..d5cb13e3d87664 100644 --- a/tests/components/open_router/test_ai_task.py +++ b/tests/components/open_router/test_ai_task.py @@ -6,9 +6,9 @@ from openai.types import CompletionUsage from openai.types.chat import ChatCompletion, ChatCompletionMessage from openai.types.chat.chat_completion import Choice +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import ai_task, media_source from homeassistant.const import Platform @@ -121,9 +121,9 @@ async def test_generate_structured_data( task_name="Test Task", entity_id="ai_task.gemini_1_5_pro", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -198,9 +198,9 @@ async def test_generate_invalid_structured_data( task_name="Test Task", entity_id="ai_task.gemini_1_5_pro", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, diff --git a/tests/components/openai_conversation/test_ai_task.py b/tests/components/openai_conversation/test_ai_task.py index ae08c3e17570e3..ad9454c13cbf94 100644 --- a/tests/components/openai_conversation/test_ai_task.py +++ b/tests/components/openai_conversation/test_ai_task.py @@ -5,8 +5,8 @@ import httpx from openai import PermissionDeniedError +import probatio import pytest -import voluptuous as vol from homeassistant.components import ai_task, media_source from homeassistant.components.openai_conversation import DOMAIN @@ -95,9 +95,9 @@ async def test_generate_structured_data( task_name="Test Task", entity_id="ai_task.openai_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, @@ -132,9 +132,9 @@ async def test_generate_invalid_structured_data( task_name="Test Task", entity_id="ai_task.openai_ai_task", instructions="Generate test data", - structure=vol.Schema( + structure=probatio.Schema( { - vol.Required("characters"): selector.selector( + probatio.Required("characters"): selector.selector( { "text": { "multiple": True, diff --git a/tests/components/openai_conversation/test_entity.py b/tests/components/openai_conversation/test_entity.py index 1297c2f90e2b24..ba976270d38b39 100644 --- a/tests/components/openai_conversation/test_entity.py +++ b/tests/components/openai_conversation/test_entity.py @@ -3,8 +3,8 @@ from pathlib import Path from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.openai_conversation.entity import ( _format_structured_output, @@ -17,16 +17,16 @@ async def test_format_structured_output() -> None: """Test the format_structured_output function.""" - schema = vol.Schema( + schema = probatio.Schema( { - vol.Required("name"): selector.TextSelector(), - vol.Optional("age"): selector.NumberSelector( + probatio.Required("name"): selector.TextSelector(), + probatio.Optional("age"): selector.NumberSelector( config=selector.NumberSelectorConfig( min=0, max=120, ), ), - vol.Required("stuff"): selector.ObjectSelector( + probatio.Required("stuff"): selector.ObjectSelector( { "multiple": True, "fields": { diff --git a/tests/components/opendisplay/test_services.py b/tests/components/opendisplay/test_services.py index f5eba1cb06512d..903533a91d1d11 100644 --- a/tests/components/opendisplay/test_services.py +++ b/tests/components/opendisplay/test_services.py @@ -13,8 +13,8 @@ BLEConnectionError, ) from PIL import Image as PILImage +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.opendisplay.const import CONF_ENCRYPTION_KEY, DOMAIN @@ -255,7 +255,7 @@ async def test_upload_image_invalid_mode( """Test that invalid mode strings are rejected by the schema.""" device_id = _device_id(hass, mock_config_entry) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "upload_image", diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index eeee4e9882f0f7..00386eb215ba3d 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -2,9 +2,9 @@ from unittest.mock import AsyncMock, patch +import probatio from pyopenuv.errors import InvalidApiKeyError import pytest -import voluptuous as vol from homeassistant.components.openuv.const import ( CONF_FROM_WINDOW, @@ -78,9 +78,9 @@ async def test_options_flow( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" - def get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: + def get_schema_marker(data_schema: probatio.Schema, key: str) -> probatio.Marker: for k in data_schema.schema: - if k == key and isinstance(k, vol.Marker): + if k == key and isinstance(k, probatio.Marker): return k return None diff --git a/tests/components/orvibo/test_config_flow.py b/tests/components/orvibo/test_config_flow.py index 563f4c17c2573c..5766833f7a9885 100644 --- a/tests/components/orvibo/test_config_flow.py +++ b/tests/components/orvibo/test_config_flow.py @@ -5,8 +5,8 @@ from unittest.mock import patch from orvibo.s20 import S20Exception +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.orvibo.const import CONF_SWITCH_LIST, DEFAULT_NAME, DOMAIN @@ -299,7 +299,7 @@ async def test_discover_skips_existing_and_invalid_mac( assert result["step_id"] == "choose_switch" schema = result["data_schema"].schema - dropdown_options = schema[vol.Required(CONF_SWITCH_LIST)].container + dropdown_options = schema[probatio.Required(CONF_SWITCH_LIST)].container assert "192.168.1.12" in dropdown_options assert "192.168.1.10" not in dropdown_options diff --git a/tests/components/peblar/test_services.py b/tests/components/peblar/test_services.py index 220d2187add2e9..c0f87639fc9027 100644 --- a/tests/components/peblar/test_services.py +++ b/tests/components/peblar/test_services.py @@ -10,8 +10,8 @@ PeblarRfidToken, PeblarVehicleToken, ) +import probatio import pytest -import voluptuous as vol from homeassistant.components.peblar.const import DOMAIN from homeassistant.components.peblar.services import ( @@ -456,7 +456,7 @@ async def test_authorize_charge_session_needs_exactly_one_token( service_data: dict[str, Any], ) -> None: """Test the charger is told which token to present, and only one.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_AUTHORIZE_CHARGE_SESSION, diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index 16a193139b4f70..57e070c1d1901f 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -3,8 +3,8 @@ from unittest.mock import patch from peco import HttpError, IncompatibleMeterError, UnresponsiveMeterError +from probatio.error import Invalid import pytest -from voluptuous.error import Invalid from homeassistant import config_entries from homeassistant.components.peco.const import DOMAIN diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index bd6f10f2573b5a..c69272f2eeae63 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -5,8 +5,8 @@ import socket from unittest.mock import patch +from probatio import MultipleInvalid import pytest -from voluptuous import MultipleInvalid from homeassistant.components import pilight from homeassistant.core import HomeAssistant diff --git a/tests/components/portainer/test_services.py b/tests/components/portainer/test_services.py index 85e127bcfab8ff..526553bb81f7c9 100644 --- a/tests/components/portainer/test_services.py +++ b/tests/components/portainer/test_services.py @@ -3,13 +3,13 @@ from datetime import timedelta from unittest.mock import AsyncMock, MagicMock +from probatio import MultipleInvalid from pyportainer import ( PortainerAuthenticationError, PortainerConnectionError, PortainerTimeoutError, ) import pytest -from voluptuous import MultipleInvalid from homeassistant.components.portainer.const import DOMAIN from homeassistant.components.portainer.services import ( diff --git a/tests/components/random/test_config_flow.py b/tests/components/random/test_config_flow.py index b4eff5c966b1dc..9c51b7b65c1351 100644 --- a/tests/components/random/test_config_flow.py +++ b/tests/components/random/test_config_flow.py @@ -3,8 +3,8 @@ from typing import Any from unittest.mock import patch +from probatio import Invalid import pytest -from voluptuous import Invalid from homeassistant import config_entries from homeassistant.components.random import async_setup_entry diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index b734450451971f..a0c14d6519a497 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -7,10 +7,10 @@ from unittest.mock import patch from freezegun import freeze_time +from probatio.error import MultipleInvalid import pytest from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session -from voluptuous.error import MultipleInvalid from homeassistant.components.recorder import DOMAIN, Recorder from homeassistant.components.recorder.const import SupportedDialect diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 0c8a60e2a90b60..74aa269f71b762 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -6,9 +6,9 @@ from typing import Any from unittest.mock import ANY, Mock, patch +import probatio import pytest from sqlalchemy import select -import voluptuous as vol from homeassistant import exceptions from homeassistant.components import recorder @@ -4590,7 +4590,7 @@ async def test_get_statistics_service_missing_mandatory_keys( await async_recorder_block_till_done(hass) with pytest.raises( - vol.error.MultipleInvalid, + probatio.error.MultipleInvalid, match=re.escape(f"required key not provided at '{missing_key}'"), ): await hass.services.async_call( diff --git a/tests/components/remember_the_milk/test_config_flow.py b/tests/components/remember_the_milk/test_config_flow.py index 975a4ef320ff8e..f2dfd72b237ff7 100644 --- a/tests/components/remember_the_milk/test_config_flow.py +++ b/tests/components/remember_the_milk/test_config_flow.py @@ -7,8 +7,8 @@ from unittest.mock import AsyncMock, MagicMock, patch from aiortm import AioRTMError, AuthError +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.remember_the_milk.config_flow import TOKEN_TIMEOUT_SEC @@ -47,7 +47,7 @@ def ignore_missing_translations(request: pytest.FixtureRequest) -> list[str]: return [] -def get_suggested_value(data_schema: vol.Schema, key: str) -> Any: +def get_suggested_value(data_schema: probatio.Schema, key: str) -> Any: """Return the suggested value for a key in a data schema.""" for schema_key in data_schema.schema: if schema_key == key: diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index b3d60ec576775b..f4abff4159345a 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -7,8 +7,8 @@ from unittest.mock import ANY, AsyncMock, Mock import orjson +import probatio import pytest -import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.repairs import FlowType, RepairsFlow, RepairsFlowResult @@ -122,7 +122,9 @@ async def async_step_custom_step( if user_input is not None: return self.async_create_entry(data={}) - return self.async_show_form(step_id="custom_step", data_schema=vol.Schema({})) + return self.async_show_form( + step_id="custom_step", data_schema=probatio.Schema({}) + ) class MockFixFlowAbort(RepairsFlow): diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 09233a70904521..04ae9cfa5f5e9b 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -3,8 +3,8 @@ import logging from unittest.mock import Mock +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from homeassistant.components.rflink import ( CONF_KEEPALIVE_IDLE, diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index a455253a3a8664..4254d5c5e04e50 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -2,8 +2,8 @@ from unittest.mock import PropertyMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.risco.config_flow import ( @@ -461,14 +461,14 @@ async def test_ha_to_risco_schema(hass: HomeAssistant) -> None: ) # Test an HA state that isn't used - with pytest.raises(vol.error.Invalid): + with pytest.raises(probatio.error.Invalid): await hass.config_entries.options.async_configure( result["flow_id"], user_input={**TEST_HA_TO_RISCO, "armed_custom_bypass": "D"}, ) # Test a combo that can't be selected - with pytest.raises(vol.error.Invalid): + with pytest.raises(probatio.error.Invalid): await hass.config_entries.options.async_configure( result["flow_id"], user_input={**TEST_HA_TO_RISCO, "armed_night": "A"}, diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index f593ecd513aa49..45a55be9368b32 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -4,11 +4,11 @@ from unittest.mock import Mock, patch from freezegun.api import FrozenDateTimeFactory +import probatio from pyschlage.code import AccessCode from pyschlage.exceptions import Error as SchlageError import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState from homeassistant.components.schlage.const import ( @@ -247,7 +247,7 @@ async def test_add_code_service_invalid_code( """Test add_code service rejects invalid PIN codes.""" mock_lock.access_codes = {} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_ADD_CODE, diff --git a/tests/components/select/test_condition.py b/tests/components/select/test_condition.py index ee0449695f2587..9834b6c6b56708 100644 --- a/tests/components/select/test_condition.py +++ b/tests/components/select/test_condition.py @@ -3,8 +3,8 @@ from contextlib import AbstractContextManager, nullcontext as does_not_raise from typing import Any +import probatio import pytest -import voluptuous as vol from homeassistant.components.select.condition import CONDITIONS, CONF_OPTION from homeassistant.const import CONF_ENTITY_ID, CONF_OPTIONS, CONF_TARGET @@ -274,13 +274,13 @@ async def test_select_condition_evaluates_both_domains( "select.is_option_selected", # Empty option list {CONF_OPTION: []}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( "select.is_option_selected", # Missing CONF_OPTION {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 4d3e08e3635898..3505e431e145d6 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -4,9 +4,9 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory +from probatio import MultipleInvalid import pytest from syrupy.assertion import SnapshotAssertion -from voluptuous import MultipleInvalid from homeassistant.components.climate import ( ATTR_FAN_MODE, diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 62675d89192586..81afddc99b44d3 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -7,9 +7,9 @@ from typing import Any from unittest.mock import patch +from probatio.error import MultipleInvalid import pytest from sharkiq import AylaApi, SharkIqAuthError, SharkIqNotAuthedError, SharkIqVacuum -from voluptuous.error import MultipleInvalid from homeassistant import exceptions from homeassistant.components.homeassistant import ( diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index 5f0294c2c3a086..2c081233cbfd38 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -7,10 +7,10 @@ import tempfile from unittest.mock import patch +import probatio from pysignalclirestapi.api import SignalCliRestApiError import pytest from requests_mock.mocker import Mocker -import voluptuous as vol from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant @@ -129,7 +129,7 @@ def test_send_message_to_api_with_bad_data_throws_error( assert "send message" in str(exc.value).lower() -def test_send_message_with_bad_data_throws_vol_error( +def test_send_message_with_bad_data_throws_probatio_error( signal_notification_service: SignalNotificationService, signal_requests_mock_factory: Mocker, caplog: pytest.LogCaptureFixture, @@ -139,7 +139,7 @@ def test_send_message_with_bad_data_throws_vol_error( caplog.at_level( logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" ), - pytest.raises(vol.Invalid) as exc, + pytest.raises(probatio.Invalid) as exc, ): signal_notification_service.send_message(MESSAGE, data={"test": "test"}) @@ -147,7 +147,7 @@ def test_send_message_with_bad_data_throws_vol_error( assert "not a valid option" in str(exc.value) -def test_send_message_styled_with_bad_data_throws_vol_error( +def test_send_message_styled_with_bad_data_throws_probatio_error( signal_notification_service: SignalNotificationService, signal_requests_mock_factory: Mocker, caplog: pytest.LogCaptureFixture, @@ -157,7 +157,7 @@ def test_send_message_styled_with_bad_data_throws_vol_error( caplog.at_level( logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" ), - pytest.raises(vol.Invalid) as exc, + pytest.raises(probatio.Invalid) as exc, ): signal_notification_service.send_message(MESSAGE, data={"text_mode": "test"}) diff --git a/tests/components/sofar/test_services.py b/tests/components/sofar/test_services.py index 8ce3c719ef1995..ac5a1f6c3590f2 100644 --- a/tests/components/sofar/test_services.py +++ b/tests/components/sofar/test_services.py @@ -4,8 +4,8 @@ from modbus_connection import ModbusError from modbus_connection.mock import MockModbusConnection +import probatio import pytest -import voluptuous as vol from homeassistant.components.sofar.const import DOMAIN from homeassistant.components.sofar.services import ( @@ -292,7 +292,7 @@ async def test_value_past_the_selector_bounds_is_refused( """Test the schema bounds a scripted call, which skips the selectors.""" entry, _ = await _setup_hybrid(hass) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, service, diff --git a/tests/components/sql/test_init.py b/tests/components/sql/test_init.py index 6ca90fcd55db7f..9bb837d837d179 100644 --- a/tests/components/sql/test_init.py +++ b/tests/components/sql/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.recorder import CONF_DB_URL, Recorder from homeassistant.components.sensor import ( @@ -72,19 +72,19 @@ async def test_setup_invalid_config( async def test_invalid_query(hass: HomeAssistant) -> None: """Test invalid query.""" - with pytest.raises(vol.Invalid, match="SQL query must be of type SELECT"): + with pytest.raises(probatio.Invalid, match="SQL query must be of type SELECT"): validate_sql_select(Template("DROP TABLE *", hass)) - with pytest.raises(vol.Invalid, match="SQL query is empty or unknown type"): + with pytest.raises(probatio.Invalid, match="SQL query is empty or unknown type"): validate_sql_select(Template("SELECT5 as value", hass)) - with pytest.raises(vol.Invalid, match="SQL query is empty or unknown type"): + with pytest.raises(probatio.Invalid, match="SQL query is empty or unknown type"): validate_sql_select(Template(";;", hass)) async def test_query_no_read_only(hass: HomeAssistant) -> None: """Test query no read only.""" - with pytest.raises(vol.Invalid, match="SQL query must be of type SELECT"): + with pytest.raises(probatio.Invalid, match="SQL query must be of type SELECT"): validate_sql_select( Template("UPDATE states SET state = 999999 WHERE state_id = 11125", hass) ) @@ -92,7 +92,7 @@ async def test_query_no_read_only(hass: HomeAssistant) -> None: async def test_query_no_read_only_cte(hass: HomeAssistant) -> None: """Test query no read only CTE.""" - with pytest.raises(vol.Invalid, match="SQL query must be of type SELECT"): + with pytest.raises(probatio.Invalid, match="SQL query must be of type SELECT"): validate_sql_select( Template( "WITH test AS (SELECT state FROM states)" @@ -104,7 +104,9 @@ async def test_query_no_read_only_cte(hass: HomeAssistant) -> None: async def test_multiple_queries(hass: HomeAssistant) -> None: """Test multiple queries.""" - with pytest.raises(vol.Invalid, match="Multiple SQL statements are not allowed"): + with pytest.raises( + probatio.Invalid, match="Multiple SQL statements are not allowed" + ): validate_sql_select( Template("SELECT 5 as value; UPDATE states SET state = 10;", hass) ) diff --git a/tests/components/sql/test_services.py b/tests/components/sql/test_services.py index 8a6a1907cc4df6..8c333ba5334940 100644 --- a/tests/components/sql/test_services.py +++ b/tests/components/sql/test_services.py @@ -4,9 +4,8 @@ import sqlite3 from unittest.mock import patch +import probatio import pytest -import voluptuous as vol -from voluptuous import MultipleInvalid from homeassistant.components.recorder import Recorder from homeassistant.components.sql.const import DOMAIN @@ -184,7 +183,7 @@ async def test_query_service_invalid_query_not_select( await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - with pytest.raises(vol.Invalid, match="SQL query must be of type SELECT"): + with pytest.raises(probatio.Invalid, match="SQL query must be of type SELECT"): await hass.services.async_call( DOMAIN, SERVICE_QUERY, @@ -202,7 +201,9 @@ async def test_query_service_sqlalchemy_error( await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - with pytest.raises(MultipleInvalid, match="SQL query is empty or unknown type"): + with pytest.raises( + probatio.MultipleInvalid, match="SQL query is empty or unknown type" + ): await hass.services.async_call( DOMAIN, SERVICE_QUERY, diff --git a/tests/components/sql/test_util.py b/tests/components/sql/test_util.py index 7118c9252ed27c..e1c2f0c3bf6aa1 100644 --- a/tests/components/sql/test_util.py +++ b/tests/components/sql/test_util.py @@ -3,8 +3,8 @@ from datetime import UTC, date, datetime from decimal import Decimal +import probatio import pytest -import voluptuous as vol from homeassistant.components.recorder import Recorder, get_instance from homeassistant.components.sql.util import ( @@ -71,7 +71,7 @@ async def test_invalid_sql_queries( expected_error_message: str, ) -> None: """Test that invalid SQL queries raise the correct exception.""" - with pytest.raises(vol.Invalid, match=expected_error_message): + with pytest.raises(probatio.Invalid, match=expected_error_message): validate_sql_select(Template(sql_query, hass)) diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index f9222e4bacfb22..ef6830ec71db94 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -3,8 +3,8 @@ from unittest import mock from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import statsd from homeassistant.const import STATE_OFF, STATE_ON @@ -23,9 +23,9 @@ def test_invalid_config() -> None: """Test configuration with defaults.""" config = {"statsd": {"host1": "host1"}} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): statsd.CONFIG_SCHEMA(None) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): statsd.CONFIG_SCHEMA(config) diff --git a/tests/components/subaru/test_lock.py b/tests/components/subaru/test_lock.py index aba40b2ac69f5b..256ab5dfe7d59e 100644 --- a/tests/components/subaru/test_lock.py +++ b/tests/components/subaru/test_lock.py @@ -2,9 +2,9 @@ from unittest.mock import patch +from probatio.error import MultipleInvalid import pytest from subarulink import SubaruException -from voluptuous.error import MultipleInvalid from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.subaru.const import ( diff --git a/tests/components/sun/test_condition.py b/tests/components/sun/test_condition.py index 0860349d793110..b955e24a53cd36 100644 --- a/tests/components/sun/test_condition.py +++ b/tests/components/sun/test_condition.py @@ -4,8 +4,8 @@ from datetime import datetime, timedelta from freezegun import freeze_time +import probatio import pytest -import voluptuous as vol from homeassistant.components import automation from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET @@ -1457,7 +1457,7 @@ async def test_sun_state_condition_takes_no_options( ) -> None: """Test the sun state conditions accept no target and reject options.""" await async_validate_condition_config(hass, {"condition": condition_key}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_condition_config( hass, {"condition": condition_key, "options": {"unknown": True}} ) @@ -1917,11 +1917,11 @@ async def test_midnight_sun_polar_night_condition_flips_at_crossing( ("sun.is_golden_hour", "any", nullcontext()), ("sun.is_golden_hour", "morning", nullcontext()), ("sun.is_golden_hour", "evening", nullcontext()), - ("sun.is_golden_hour", "invalid", pytest.raises(vol.Invalid)), + ("sun.is_golden_hour", "invalid", pytest.raises(probatio.Invalid)), ("sun.is_blue_hour", "any", nullcontext()), ("sun.is_blue_hour", "morning", nullcontext()), ("sun.is_blue_hour", "evening", nullcontext()), - ("sun.is_blue_hour", "invalid", pytest.raises(vol.Invalid)), + ("sun.is_blue_hour", "invalid", pytest.raises(probatio.Invalid)), ], ) async def test_golden_blue_hour_condition_period_validation( diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 673aa3d72e031c..b0e30c6c66761b 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -7,8 +7,8 @@ import astral from astral.sun import elevation as astral_elevation from freezegun import freeze_time +import probatio import pytest -import voluptuous as vol from homeassistant.components import automation, sun from homeassistant.components.sun.trigger import _next_polar_transition @@ -1052,7 +1052,7 @@ async def test_dawn_dusk_twilight_validation( if valid: await async_validate_trigger_config(hass, [config]) else: - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_trigger_config(hass, [config]) @@ -1062,11 +1062,11 @@ async def test_dawn_dusk_twilight_validation( ("sun.golden_hour_started", "any", nullcontext()), ("sun.golden_hour_started", "morning", nullcontext()), ("sun.golden_hour_started", "evening", nullcontext()), - ("sun.golden_hour_started", "invalid", pytest.raises(vol.Invalid)), + ("sun.golden_hour_started", "invalid", pytest.raises(probatio.Invalid)), ("sun.blue_hour_ended", "any", nullcontext()), ("sun.blue_hour_ended", "morning", nullcontext()), ("sun.blue_hour_ended", "evening", nullcontext()), - ("sun.blue_hour_ended", "invalid", pytest.raises(vol.Invalid)), + ("sun.blue_hour_ended", "invalid", pytest.raises(probatio.Invalid)), ], ) async def test_golden_blue_hour_period_validation( diff --git a/tests/components/swiss_public_transport/test_services.py b/tests/components/swiss_public_transport/test_services.py index 7bdd4ba4b2bd59..235001a59d5ed2 100644 --- a/tests/components/swiss_public_transport/test_services.py +++ b/tests/components/swiss_public_transport/test_services.py @@ -7,8 +7,8 @@ OpendataTransportConnectionError, OpendataTransportError, ) +import probatio import pytest -from voluptuous import error as vol_er from homeassistant.components.swiss_public_transport.const import ( ATTR_LIMIT, @@ -90,12 +90,12 @@ async def test_service_call_fetch_connections_success( @pytest.mark.parametrize( ("limit", "config_data", "expected_result", "raise_error"), [ - (-1, MOCK_DATA_STEP_BASE, pytest.raises(vol_er.MultipleInvalid), None), - (0, MOCK_DATA_STEP_BASE, pytest.raises(vol_er.MultipleInvalid), None), + (-1, MOCK_DATA_STEP_BASE, pytest.raises(probatio.error.MultipleInvalid), None), + (0, MOCK_DATA_STEP_BASE, pytest.raises(probatio.error.MultipleInvalid), None), ( CONNECTIONS_MAX + 1, MOCK_DATA_STEP_BASE, - pytest.raises(vol_er.MultipleInvalid), + pytest.raises(probatio.error.MultipleInvalid), None, ), ( diff --git a/tests/components/switchbot_cloud/test_services.py b/tests/components/switchbot_cloud/test_services.py index 949d247aea2a0e..a2bc3cbf6657fd 100644 --- a/tests/components/switchbot_cloud/test_services.py +++ b/tests/components/switchbot_cloud/test_services.py @@ -2,9 +2,9 @@ from unittest.mock import AsyncMock, patch +import probatio import pytest from switchbot_api import SwitchBotAPI -import voluptuous as vol from homeassistant.components.switchbot_cloud.const import ( AI_ART_FRAME_UPLOAD_IMAGE_SERVICE, @@ -74,7 +74,7 @@ async def test_upload_image_no_device_id_raises( ) -> None: """Test service raises when no device_id is provided.""" await _setup(hass, mock_list_devices, mock_get_status) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, AI_ART_FRAME_UPLOAD_IMAGE_SERVICE, diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index c6fa2d9afb9a9f..166a15a00a5444 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -9,8 +9,8 @@ get_topic_tele_state, get_topic_tele_will, ) +from probatio import MultipleInvalid import pytest -from voluptuous import MultipleInvalid from homeassistant.components import fan from homeassistant.components.tasmota.const import DEFAULT_PREFIX diff --git a/tests/components/template/conftest.py b/tests/components/template/conftest.py index d22e8c163fbf41..6d4864fbb67955 100644 --- a/tests/components/template/conftest.py +++ b/tests/components/template/conftest.py @@ -5,8 +5,8 @@ from itertools import chain from unittest.mock import AsyncMock, Mock +import probatio import pytest -import voluptuous as vol from homeassistant.components import template from homeassistant.components.device_automation import toggle_entity @@ -124,7 +124,7 @@ async def _async_get_actions( f"{FAKE_DOMAIN}.device_action", Mock( ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend( - {vol.Required("domain"): FAKE_DOMAIN} + {probatio.Required("domain"): FAKE_DOMAIN} ), async_get_actions=_async_get_actions, async_call_action_from_config=AsyncMock(), diff --git a/tests/components/template/test_config.py b/tests/components/template/test_config.py index 5d3db0b1c4ce7d..d0a7945925ea51 100644 --- a/tests/components/template/test_config.py +++ b/tests/components/template/test_config.py @@ -1,7 +1,7 @@ """Test Template config.""" +import probatio import pytest -import voluptuous as vol from homeassistant.components.template import DOMAIN, PLATFORMS from homeassistant.components.template.config import ( @@ -134,7 +134,7 @@ async def test_platform_device_tracker_creates_issue( ) async def test_invalid_schema(hass: HomeAssistant, config: dict) -> None: """Test invalid config schemas.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): CONFIG_SECTION_SCHEMA(config) @@ -178,7 +178,7 @@ async def test_invalid_default_entity_id( "default_entity_id": default_entity_id, }, } - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): CONFIG_SECTION_SCHEMA(config) diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 45884552ea605b..ad0a4e1bb7e5cf 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -4,9 +4,9 @@ from itertools import chain from typing import Any +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import fan, template from homeassistant.components.fan import ( @@ -1096,7 +1096,7 @@ async def test_set_invalid_osc_from_initial_state( """Test set invalid oscillating when fan is in initial state.""" await async_trigger(hass, TEST_STATE_ENTITY_ID, "anything") await common.async_turn_on(hass, TEST_FAN.entity_id) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await common.async_oscillate(hass, TEST_FAN.entity_id, "invalid") _verify(hass, STATE_ON, None, None, None, None) @@ -1120,7 +1120,7 @@ async def test_set_invalid_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> await common.async_oscillate(hass, TEST_FAN.entity_id, False) _verify(hass, STATE_ON, None, False, None, None) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await common.async_oscillate(hass, TEST_FAN.entity_id, None) _verify(hass, STATE_ON, None, False, None, None) diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index 588e79aa7e7a34..12bb4749926eb0 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -17,6 +17,7 @@ from bleak.exc import BleakError from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa +import probatio import pytest from tesla_fleet_api.const import AuthorizedClientState from tesla_fleet_api.exceptions import ( @@ -33,7 +34,6 @@ from tesla_fleet_api.tesla import VehicleRouter from tesla_fleet_api.tesla.bluetooth import TeslaBluetooth from tesla_fleet_api.teslemetry.energysite import AuthorizedClient, AuthorizedClients -import voluptuous as vol from homeassistant.components.application_credentials import ( ClientCredential, @@ -1598,7 +1598,7 @@ def _credentials_host_is_blank(result: SubentryFlowResult) -> bool: """Return whether the CONF_HOST field carries no schema default (left blank).""" for key in result["data_schema"].schema: if key == CONF_HOST: - return key.default is vol.UNDEFINED + return key.default is probatio.UNDEFINED raise AssertionError("CONF_HOST field not found in credentials schema") diff --git a/tests/components/test_shared_test_helpers.py b/tests/components/test_shared_test_helpers.py index 5479a7f8d4d306..182369618155bc 100644 --- a/tests/components/test_shared_test_helpers.py +++ b/tests/components/test_shared_test_helpers.py @@ -12,8 +12,8 @@ from typing import Any, cast +import probatio import pytest -import voluptuous as vol from homeassistant.const import CONF_TARGET, STATE_ON from homeassistant.core import HomeAssistant @@ -41,9 +41,9 @@ _ValidCondition = make_entity_state_condition("test", STATE_ON) # A schema without a ``target`` marker (a class that does not expose a user target). -_NO_TARGET_SCHEMA = vol.Schema({vol.Optional("options"): dict}) +_NO_TARGET_SCHEMA = probatio.Schema({probatio.Optional("options"): dict}) # A schema that does expose the standard user target slot. -_TARGET_SCHEMA = vol.Schema({vol.Required(CONF_TARGET): cv.TARGET_FIELDS}) +_TARGET_SCHEMA = probatio.Schema({probatio.Required(CONF_TARGET): cv.TARGET_FIELDS}) class _MachineryOverrideTrigger(_ValidTrigger): diff --git a/tests/components/time_date/test_config_flow.py b/tests/components/time_date/test_config_flow.py index d2000ae2d2ab5a..6e6d245b2b2587 100644 --- a/tests/components/time_date/test_config_flow.py +++ b/tests/components/time_date/test_config_flow.py @@ -3,8 +3,8 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.components.time_date.const import DOMAIN @@ -44,7 +44,7 @@ async def test_user_flow_does_not_allow_beat(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.FORM - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.config_entries.flow.async_configure( result["flow_id"], {"display_options": ["beat"]}, diff --git a/tests/components/timer/test_trigger.py b/tests/components/timer/test_trigger.py index ce356747fe26bf..715768d39d6296 100644 --- a/tests/components/timer/test_trigger.py +++ b/tests/components/timer/test_trigger.py @@ -5,8 +5,8 @@ from typing import Any from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components.timer import ( ATTR_FINISHES_AT, @@ -325,7 +325,7 @@ async def test_time_remaining_trigger_validation(hass: HomeAssistant) -> None: ) # Missing remaining option - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_trigger_config( hass, [ diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 7af5021cd7e0fc..283fbe9a5e62ed 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -5,9 +5,9 @@ from typing import Any import zoneinfo +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components.todo import ( ATTR_DESCRIPTION, @@ -221,9 +221,9 @@ async def test_add_item_service_raises( @pytest.mark.parametrize( ("item_data", "expected_exception", "expected_error"), [ - ({}, vol.Invalid, "required key not provided"), - ({ATTR_ITEM: ""}, vol.Invalid, "length of value must be at least 1"), - ({ATTR_ITEM: " "}, vol.Invalid, "length of value must be at least 1"), + ({}, probatio.Invalid, "required key not provided"), + ({ATTR_ITEM: ""}, probatio.Invalid, "length of value must be at least 1"), + ({ATTR_ITEM: " "}, probatio.Invalid, "length of value must be at least 1"), ( {ATTR_ITEM: "Submit forms", ATTR_DESCRIPTION: "Submit tax forms"}, ServiceValidationError, @@ -548,7 +548,7 @@ async def test_update_item_service_invalid_input( await create_mock_platform(hass, [test_entity]) - with pytest.raises(vol.Invalid, match=expected_error): + with pytest.raises(probatio.Invalid, match=expected_error): await hass.services.async_call( DOMAIN, "update_item", @@ -786,7 +786,7 @@ async def test_remove_todo_item_service_invalid_input( await create_mock_platform(hass, [test_entity]) - with pytest.raises(vol.Invalid, match=r"required key not provided at 'item'"): + with pytest.raises(probatio.Invalid, match=r"required key not provided at 'item'"): await hass.services.async_call( DOMAIN, TodoServices.REMOVE_ITEM, diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index e4f08f55dbaf2c..41f67304cc07a8 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -2,10 +2,10 @@ from unittest import mock +import probatio import pytest import requests import requests_mock -import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.tomato import device_tracker as tomato @@ -230,7 +230,7 @@ def test_config_valid_verify_ssl_bool(hass: HomeAssistant, mock_session_send) -> def test_config_errors() -> None: """Test for configuration errors.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): tomato.PLATFORM_SCHEMA( { CONF_PLATFORM: DEVICE_TRACKER_DOMAIN, @@ -243,7 +243,7 @@ def test_config_errors() -> None: tomato.CONF_HTTP_ID: "0987654321", } ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): tomato.PLATFORM_SCHEMA( { CONF_PLATFORM: DEVICE_TRACKER_DOMAIN, @@ -256,7 +256,7 @@ def test_config_errors() -> None: tomato.CONF_HTTP_ID: "0987654321", } ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): tomato.PLATFORM_SCHEMA( { CONF_PLATFORM: DEVICE_TRACKER_DOMAIN, @@ -269,7 +269,7 @@ def test_config_errors() -> None: tomato.CONF_HTTP_ID: "0987654321", } ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): tomato.PLATFORM_SCHEMA( { CONF_PLATFORM: DEVICE_TRACKER_DOMAIN, @@ -282,7 +282,7 @@ def test_config_errors() -> None: tomato.CONF_HTTP_ID: "0987654321", } ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): tomato.PLATFORM_SCHEMA( { CONF_PLATFORM: DEVICE_TRACKER_DOMAIN, diff --git a/tests/components/tplink_omada/test_services.py b/tests/components/tplink_omada/test_services.py index abfad47c0dd891..b5aa34b3960ead 100644 --- a/tests/components/tplink_omada/test_services.py +++ b/tests/components/tplink_omada/test_services.py @@ -2,10 +2,10 @@ from unittest.mock import MagicMock +import probatio import pytest from tplink_omada_client import OmadaClientSettings from tplink_omada_client.exceptions import OmadaClientException, RequestFailed -import voluptuous as vol from homeassistant.components.tplink_omada.const import DOMAIN from homeassistant.components.tplink_omada.services import async_setup_services @@ -402,7 +402,7 @@ async def test_service_set_client_name_empty_name_rejected( mac = "aa:bb:cc:dd:ee:ff" device_id = _add_client_device(hass, mock_config_entry, mac) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "set_client_name", diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index 346bc542ad24f5..68d7d52baabfa7 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -6,8 +6,8 @@ from typing import Any from unittest.mock import MagicMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.tts import ( @@ -191,7 +191,11 @@ class MockTTS(MockPlatform): """A mock TTS platform.""" PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)} + { + probatio.Optional(CONF_LANG, default=DEFAULT_LANG): probatio.In( + SUPPORT_LANGUAGES + ) + } ) def __init__(self, provider: MockTTSProvider, **kwargs: Any) -> None: diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index c04145ad25fec4..2eb4f8f77e235d 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -3,8 +3,8 @@ from copy import copy from unittest.mock import Mock, patch +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from homeassistant import config as hass_config from homeassistant.components import input_number, input_select, media_player, switch diff --git a/tests/components/velbus/test_services.py b/tests/components/velbus/test_services.py index 424c87fd138147..f4b41ed4a96520 100644 --- a/tests/components/velbus/test_services.py +++ b/tests/components/velbus/test_services.py @@ -2,8 +2,8 @@ from unittest.mock import AsyncMock, MagicMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.velbus.const import ( CONF_CONFIG_ENTRY, @@ -56,7 +56,7 @@ async def test_global_services_with_config_entry( ) # Test missing interface - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SCAN, diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index 50e0b61c655f5c..941e828f547d5a 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET from homeassistant.config_entries import ConfigEntryState @@ -85,7 +85,7 @@ async def test_send_magic_packet(hass: HomeAssistant) -> None: assert "ip_address" not in mocked_wakeonlan.mock_calls[0][2] mocked_wakeonlan.reset_mock() - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_SEND_MAGIC_PACKET, diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index 33870bf14d9c2e..48d079981a0ce4 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -3,8 +3,8 @@ from unittest import mock from unittest.mock import AsyncMock, MagicMock +import probatio import pytest -import voluptuous as vol from homeassistant.components.water_heater import ( DOMAIN, @@ -40,7 +40,7 @@ async def test_set_temp_schema_no_req( calls = async_mock_service(hass, domain, service, schema) data = {"hvac_mode": "off", "entity_id": ["climate.test_id"]} - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call(domain, service, data) await hass.async_block_till_done() diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 925a33fe3572fe..c979470861579d 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -9,9 +9,9 @@ from unittest.mock import ANY, AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant import loader from homeassistant.components.device_automation import toggle_entity @@ -95,7 +95,7 @@ def fake_integration(hass: HomeAssistant): f"{DOMAIN}.device_action", Mock( ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend( - {vol.Required("domain"): DOMAIN} + {probatio.Required("domain"): DOMAIN} ), spec=["ACTION_SCHEMA"], ), @@ -722,9 +722,9 @@ async def test_call_service_schema_validation_error( """Test call service command with invalid service data.""" calls = [] - service_schema = vol.Schema( + service_schema = probatio.Schema( { - vol.Required("message"): str, + probatio.Required("message"): str, } ) @@ -3629,13 +3629,13 @@ async def test_validate_config_works( @pytest.mark.parametrize( ("key", "config", "error"), [ - # Raises vol.Invalid + # Raises probatio.Invalid ( "triggers", {"platform": "non_existing", "event_type": "hello"}, "Invalid trigger 'non_existing' specified", ), - # Raises vol.Invalid + # Raises probatio.Invalid ( "conditions", { @@ -3658,7 +3658,7 @@ async def test_validate_config_works( }, "Unknown device 'a51a57e5af051eb403d56eb9e6fd691c'", ), - # Raises vol.Invalid + # Raises probatio.Invalid ( "actions", {"non_existing": "domain_test.test_service"}, diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 4e63fc071fc76f..3f3e91cc749ee7 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -5,8 +5,8 @@ from unittest.mock import Mock, patch from aiohttp.test_utils import make_mocked_request +import probatio import pytest -import voluptuous as vol from homeassistant import exceptions from homeassistant.components import websocket_api @@ -28,7 +28,7 @@ " Mock User from 127.0.0.42 (Browser)", ), ( - vol.Invalid("Invalid something"), + probatio.Invalid("Invalid something"), websocket_api.ERR_INVALID_FORMAT, "Invalid something. Got {'id': 5}", "Error handling message: Invalid something." @@ -177,7 +177,7 @@ async def test_credential_redaction( "password": test_input[1], "token": test_input[2], } - connection.async_handle_exception(msg, vol.Invalid("bad input")) + connection.async_handle_exception(msg, probatio.Invalid("bad input")) assert len(send_messages) == 1 error_message = send_messages[0]["error"]["message"] diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index a6cc14404ee6e6..f343543735e568 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -2,7 +2,7 @@ from typing import Any -import voluptuous as vol +import probatio from homeassistant.components import http, websocket_api from homeassistant.core import HomeAssistant @@ -48,7 +48,7 @@ def get_request( handle_request(http.current_request.get(), connection, msg) @websocket_api.websocket_command( - {"type": "test-get-request-with-arg", vol.Required("arg"): str} + {"type": "test-get-request-with-arg", probatio.Required("arg"): str} ) def get_with_arg_request( hass: HomeAssistant, diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 25ddeabdbf45e2..2b0859e7f42a21 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch from aiohttp import WSMsgType -import voluptuous as vol +import probatio from homeassistant.components.websocket_api import ( async_register_command, @@ -70,14 +70,14 @@ async def test_handler_failing(hass: HomeAssistant, websocket_client) -> None: assert msg["error"]["code"] == const.ERR_UNKNOWN_ERROR -async def test_invalid_vol(hass: HomeAssistant, websocket_client) -> None: - """Test a command that raises invalid vol error.""" +async def test_invalid_probatio(hass: HomeAssistant, websocket_client) -> None: + """Test a command that raises invalid probatio error.""" async_register_command( hass, "bla", Mock(side_effect=TypeError), messages.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {"type": "bla", vol.Required("test_config"): str} + {"type": "bla", probatio.Required("test_config"): str} ), ) diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index c46e05baf86306..77272ead3a7cc6 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -6,8 +6,8 @@ from typing import TYPE_CHECKING from unittest.mock import ANY, AsyncMock, MagicMock, call, patch +import probatio import pytest -import voluptuous as vol from zha.application.const import ( ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, @@ -791,7 +791,7 @@ async def test_permit_with_install_code_fail( ) -> None: """Test permit service with install code.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_PERMIT, params, True, Context(user_id=hass_admin_user.id) ) diff --git a/tests/components/zone/test_condition.py b/tests/components/zone/test_condition.py index 3402a3f7305718..135f50cc8f8632 100644 --- a/tests/components/zone/test_condition.py +++ b/tests/components/zone/test_condition.py @@ -4,8 +4,8 @@ from typing import Any from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components.zone import condition as zone_condition from homeassistant.components.zone.condition import CONDITIONS @@ -410,7 +410,7 @@ async def test_zone_condition_rejects_non_zone_entity_id( hass: HomeAssistant, condition_key: str, config: dict[str, Any] ) -> None: """Test that the zone option must reference entities in the zone domain.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await condition.async_validate_condition_config( hass, {"condition": condition_key, **config}, diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index dc5d638ec06fc3..f5f89601c6b3c0 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -4,8 +4,8 @@ from typing import Any from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.components import automation, zone from homeassistant.components.zone.trigger import TRIGGERS @@ -572,7 +572,7 @@ async def test_zone_trigger_rejects_non_zone_entity_id( hass: HomeAssistant, trigger_key: str ) -> None: """Test that the zone option must reference entities in the zone domain.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_trigger_config( hass, [ diff --git a/tests/components/zoneminder/test_services.py b/tests/components/zoneminder/test_services.py index 1577a7082a0af5..e5ba795acf3ef8 100644 --- a/tests/components/zoneminder/test_services.py +++ b/tests/components/zoneminder/test_services.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.zoneminder.const import DOMAIN from homeassistant.const import ATTR_ID, ATTR_NAME @@ -88,7 +88,7 @@ async def test_set_run_state_missing_fields_rejected( assert await async_setup_component(hass, DOMAIN, single_server_config) await hass.async_block_till_done() - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( DOMAIN, "set_run_state", diff --git a/tests/components/zoneminder/test_switch.py b/tests/components/zoneminder/test_switch.py index 6a6619c382685e..441e9b85cef2d4 100644 --- a/tests/components/zoneminder/test_switch.py +++ b/tests/components/zoneminder/test_switch.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from zoneminder.monitor import MonitorState from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -233,9 +233,9 @@ async def test_switch_platform_not_ready_empty_monitors( def test_platform_schema_requires_command_on_off() -> None: """Test platform schema requires command_on and command_off.""" # Missing command_on - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): PLATFORM_SCHEMA({"platform": "zoneminder", "command_off": "Monitor"}) # Missing command_off - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): PLATFORM_SCHEMA({"platform": "zoneminder", "command_on": "Modect"}) diff --git a/tests/components/zwave_js/test_condition.py b/tests/components/zwave_js/test_condition.py index c714b11aef6317..b2f7f6dac3f1e1 100644 --- a/tests/components/zwave_js/test_condition.py +++ b/tests/components/zwave_js/test_condition.py @@ -3,8 +3,8 @@ from typing import Any from unittest.mock import MagicMock +import probatio import pytest -import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.event import Event from zwave_js_server.model.node import Node @@ -316,7 +316,7 @@ async def test_value_missing_on_node( ) assert checker.async_check() is False - with pytest.raises(vol.Invalid, match="No targeted node has value"): + with pytest.raises(probatio.Invalid, match="No targeted node has value"): await _checker( hass, { @@ -361,7 +361,7 @@ async def test_no_nodes_resolved( other = device_registry.async_get_or_create( config_entry_id=integration.entry_id, identifiers={("other", "1")} ) - with pytest.raises(vol.Invalid, match="No nodes found"): + with pytest.raises(probatio.Invalid, match="No nodes found"): await _checker( hass, { @@ -519,7 +519,7 @@ async def test_config_parameter_missing_on_node( ) -> None: """Test validation fails when no node in the target has the parameter.""" device_id = _device_id(device_registry, client, lock_schlage_be469, integration) - with pytest.raises(vol.Invalid, match="configuration parameter"): + with pytest.raises(probatio.Invalid, match="configuration parameter"): await _checker( hass, { @@ -547,7 +547,7 @@ async def test_condition_description_fields_match_schema( fields = description["fields"] assert set(fields) == {str(key) for key in schema} assert {name for name, field in fields.items() if field["required"]} == { - str(key) for key in schema if isinstance(key, vol.Required) + str(key) for key in schema if isinstance(key, probatio.Required) } diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 3b3aea04965d1b..734f83545b526c 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -12,8 +12,8 @@ from aiohasupervisor import SupervisorError from aiohasupervisor.models import AddonsOptions, Discovery import aiohttp +from probatio import InInvalid import pytest -from voluptuous import InInvalid from zwave_js_server.exceptions import ConnectionFailed, FailedCommand from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo diff --git a/tests/components/zwave_js/test_config_validation.py b/tests/components/zwave_js/test_config_validation.py index cebbde3c9b13f0..d35de99169fec0 100644 --- a/tests/components/zwave_js/test_config_validation.py +++ b/tests/components/zwave_js/test_config_validation.py @@ -2,8 +2,8 @@ from typing import Any +import probatio import pytest -import voluptuous as vol from homeassistant.components.zwave_js.config_validation import VALUE_SCHEMA, boolean @@ -27,5 +27,5 @@ def test_validation(test_cases: list[Any], expected_value: Any) -> None: @pytest.mark.parametrize("value", ["invalid", "1", "0", 1, 0]) def test_invalid_boolean_validation(value: str | int) -> None: """Test invalid cases for boolean config validator.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): boolean(value) diff --git a/tests/components/zwave_js/test_credential_services.py b/tests/components/zwave_js/test_credential_services.py index dd6e22f1492ad8..9f3e011b5e8566 100644 --- a/tests/components/zwave_js/test_credential_services.py +++ b/tests/components/zwave_js/test_credential_services.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock, create_autospec +import probatio import pytest -import voluptuous as vol from zwave_js_server.const.command_class.access_control import ( SetCredentialResult, SetUserResult, @@ -1124,7 +1124,7 @@ async def test_set_credential_requires_user_id( """Test set_credential rejects calls without user_id.""" api = _mock_access_control(lock_schlage_be469) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "set_credential", @@ -1473,7 +1473,7 @@ async def test_set_credential_id_range_validation( } payload[field] = value - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "set_credential", @@ -1498,7 +1498,7 @@ async def test_delete_user_rejects_oversize_user_id( """Reject user_id above uint16 max on delete_user.""" api = _mock_access_control(lock_schlage_be469) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "delete_user", diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index ea3023a337a34c..713ee46d812c2c 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -2,9 +2,8 @@ from unittest.mock import patch -from probatio import to_field_list +import probatio import pytest -import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.event import Event @@ -455,7 +454,7 @@ async def test_get_condition_capabilities_node_status( }, ) assert capabilities and "extra_fields" in capabilities - assert to_field_list( + assert probatio.to_field_list( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ { @@ -508,7 +507,7 @@ async def test_get_condition_capabilities_value( ("134", "Version"), ] - assert to_field_list( + assert probatio.to_field_list( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ { @@ -553,7 +552,7 @@ async def test_get_condition_capabilities_config_parameter( ) assert capabilities and "extra_fields" in capabilities - assert to_field_list( + assert probatio.to_field_list( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ { @@ -584,7 +583,7 @@ async def test_get_condition_capabilities_config_parameter( ) assert capabilities and "extra_fields" in capabilities - assert to_field_list( + assert probatio.to_field_list( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ { @@ -692,7 +691,7 @@ async def test_get_value_from_config_failure( hass: HomeAssistant, client, hank_binary_switch, integration ) -> None: """Test get_value_from_config invalid value ID.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): get_zwave_value_from_config( hank_binary_switch, { diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 01ad16c4b0a7de..dc6fb7aaabbde2 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -2,8 +2,8 @@ import copy +from probatio.error import MultipleInvalid import pytest -from voluptuous.error import MultipleInvalid from zwave_js_server.const import CommandClass from zwave_js_server.event import Event from zwave_js_server.model.node import Node diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py index ff4bbf04da9a9e..3cae26c1a0b5d7 100644 --- a/tests/components/zwave_js/test_helpers.py +++ b/tests/components/zwave_js/test_helpers.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from zwave_js_server.const import SecurityClass from zwave_js_server.model.controller import ProvisioningEntry @@ -82,7 +82,7 @@ async def test_get_value_state_schema_boolean_config_value( schema_validator = get_value_state_schema( aeon_smart_switch_6.values["102-112-0-255"] ) - assert isinstance(schema_validator, vol.Coerce) + assert isinstance(schema_validator, probatio.Coerce) assert schema_validator.type is bool diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index f0ffd0bcaa00e7..bcf99802e91dac 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock, patch +import probatio import pytest -import voluptuous as vol from zwave_js_server.exceptions import FailedZWaveCommand from zwave_js_server.model.value import SetConfigParameterResult @@ -331,7 +331,7 @@ async def test_set_config_parameter( client.async_send_command_no_wait.reset_mock() # Test that we can't include a bitmask value if parameter is a string - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -380,7 +380,7 @@ async def test_set_config_parameter( ) # Test that we can't include bitmask and value size and value format - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -396,7 +396,7 @@ async def test_set_config_parameter( ) # Test that value size must be 1, 2, or 4 (not 3) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -478,7 +478,7 @@ async def test_set_config_parameter( client.async_send_command.reset_mock() # Test setting config parameter with no valid nodes raises Exception - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SET_CONFIG_PARAMETER, @@ -963,7 +963,7 @@ async def test_refresh_value( client.async_send_command.reset_mock() # Test polling against an invalid entity raises MultipleInvalid - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_REFRESH_VALUE, @@ -1140,7 +1140,7 @@ async def test_set_value( client.async_send_command.reset_mock() # Test missing device and entities keys - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_SET_VALUE, @@ -1467,7 +1467,7 @@ async def test_multicast_set_value( client.async_send_command_no_wait.reset_mock() # Test no device, entity, or broadcast flag raises error - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_MULTICAST_SET_VALUE, @@ -1532,7 +1532,7 @@ async def test_multicast_set_value( diff_network_node.client.driver.controller.home_id.return_value = "diff_home_id" with ( - pytest.raises(vol.MultipleInvalid), + pytest.raises(probatio.MultipleInvalid), patch( "homeassistant.components.zwave_js.helpers.async_get_node_from_device_id", side_effect=(climate_danfoss_lc_13, diff_network_node), @@ -1558,7 +1558,7 @@ async def test_multicast_set_value( # without devices or entities new_entry = MockConfigEntry(domain=DOMAIN) new_entry.add_to_hass(hass) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_MULTICAST_SET_VALUE, @@ -1790,7 +1790,7 @@ async def test_ping( client.async_send_command.reset_mock() # Test no device or entity raises error - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, SERVICE_PING, diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index d1637899d39c14..b8335f491ee3d2 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -4,8 +4,8 @@ import copy from unittest.mock import MagicMock, patch +import probatio import pytest -import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.event import Event from zwave_js_server.model.node import Node @@ -1024,7 +1024,7 @@ async def test_zwave_js_event_invalid_config_entry_id( async def test_invalid_trigger_configs(hass: HomeAssistant) -> None: """Test invalid trigger configs.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await TRIGGERS["event"].async_validate_complete_config( hass, { @@ -1037,7 +1037,7 @@ async def test_invalid_trigger_configs(hass: HomeAssistant) -> None: }, ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await TRIGGERS["value_updated"].async_validate_complete_config( hass, { @@ -1058,21 +1058,21 @@ async def test_invalid_trigger_configs(hass: HomeAssistant) -> None: "controller", "inclusion started", ["config_entry_id", "device_id"], - pytest.raises(vol.Invalid, match="must not contain"), + pytest.raises(probatio.Invalid, match="must not contain"), id="controller_with_device_id", ), pytest.param( "driver", "logging", ["config_entry_id", "entity_id"], - pytest.raises(vol.Invalid, match="must not contain"), + pytest.raises(probatio.Invalid, match="must not contain"), id="driver_with_entity_id", ), pytest.param( "node", "interview stage completed", [], - pytest.raises(vol.Invalid, match="must contain"), + pytest.raises(probatio.Invalid, match="must contain"), id="node_without_targets", ), pytest.param( @@ -1086,7 +1086,7 @@ async def test_invalid_trigger_configs(hass: HomeAssistant) -> None: "controller", "inclusion started", [], - pytest.raises(vol.Invalid, match="must contain config_entry_id"), + pytest.raises(probatio.Invalid, match="must contain config_entry_id"), id="controller_without_config_entry", ), ], @@ -1652,12 +1652,14 @@ async def test_value_updated_command_class_options(hass: HomeAssistant) -> None: ) @pytest.mark.usefixtures("integration") async def test_trigger_description_fields_match_schema( - hass: HomeAssistant, trigger_type: str, options_schema: dict[vol.Marker, object] + hass: HomeAssistant, + trigger_type: str, + options_schema: dict[probatio.Marker, object], ) -> None: """Test the described fields match the trigger's options schema.""" descriptions = await trigger.async_get_all_descriptions(hass) fields = descriptions[trigger_type]["fields"] assert set(fields) == {str(key) for key in options_schema} assert {name for name, field in fields.items() if field["required"]} == { - str(key) for key in options_schema if isinstance(key, vol.Required) + str(key) for key in options_schema if isinstance(key, probatio.Required) } diff --git a/tests/hassfest/test_translations.py b/tests/hassfest/test_translations.py index b746c616b90848..eb4c09028bb3ce 100644 --- a/tests/hassfest/test_translations.py +++ b/tests/hassfest/test_translations.py @@ -1,7 +1,7 @@ """Tests for hassfest translations.""" +import probatio import pytest -import voluptuous as vol from script.hassfest import translations from script.hassfest.model import Config @@ -11,9 +11,9 @@ def test_string_with_no_placeholders_in_single_quotes() -> None: """Test string with no placeholders in single quotes.""" - schema = vol.Schema(translations.string_no_single_quoted_placeholders) + schema = probatio.Schema(translations.string_no_single_quoted_placeholders) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("This has '{placeholder}' in single quotes") for value in ( @@ -435,9 +435,9 @@ def test_gen_strings_schema( ) def test_no_placeholders_used_for_urls(translation_string: str) -> None: """Test that translation strings containing URLs are rejected.""" - schema = vol.Schema(translations.translation_value_validator) + schema = probatio.Schema(translations.translation_value_validator) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(translation_string) @@ -453,7 +453,7 @@ def test_no_placeholders_used_for_urls(translation_string: str) -> None: ) def test_allow_urls_in_translation_value(translation_string: str) -> None: """Test that URLs are allowed when allow_urls=True.""" - schema = vol.Schema( + schema = probatio.Schema( translations.custom_translation_value_validator(allow_urls=True) ) diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index 20c3d93bda5eab..4dc19c1242dd1c 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -2,8 +2,8 @@ from pathlib import Path +import probatio import pytest -import voluptuous as vol from script.hassfest.manifest import ( CUSTOM_INTEGRATION_MANIFEST_SCHEMA, @@ -45,11 +45,11 @@ def test_validate_custom_integration_manifest(integration: Integration) -> None: """Test validate custom integration manifest.""" integration.manifest["version"] = "lorem_ipsum" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) integration.manifest["version"] = None - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) integration.manifest["version"] = "1" diff --git a/tests/helpers/template/test_init.py b/tests/helpers/template/test_init.py index 44a23497991919..2f13c03b4c6f1a 100644 --- a/tests/helpers/template/test_init.py +++ b/tests/helpers/template/test_init.py @@ -6,8 +6,8 @@ from unittest.mock import patch from freezegun import freeze_time +import probatio import pytest -import voluptuous as vol from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -882,10 +882,10 @@ async def test_is_static_still_ast_evals(hass: HomeAssistant) -> None: async def test_result_wrappers(hass: HomeAssistant) -> None: """Test result wrappers.""" for text, native, orig_type, schema in ( - ("[1, 2]", [1, 2], list, vol.Schema([int])), - ("{1, 2}", {1, 2}, set, vol.Schema({int})), - ("(1, 2)", (1, 2), tuple, vol.ExactSequence([int, int])), - ('{"hello": True}', {"hello": True}, dict, vol.Schema({"hello": bool})), + ("[1, 2]", [1, 2], list, probatio.Schema([int])), + ("{1, 2}", {1, 2}, set, probatio.Schema({int})), + ("(1, 2)", (1, 2), tuple, probatio.ExactSequence([int, int])), + ('{"hello": True}', {"hello": True}, dict, probatio.Schema({"hello": bool})), ): result = render(hass, text) assert isinstance(result, orig_type) diff --git a/tests/helpers/test_automation.py b/tests/helpers/test_automation.py index 3c4b007c3fbb7e..00a1e82f0032df 100644 --- a/tests/helpers/test_automation.py +++ b/tests/helpers/test_automation.py @@ -1,7 +1,7 @@ """Test automation helpers.""" +import probatio import pytest -import voluptuous as vol from homeassistant.helpers.automation import ( get_absolute_description_key, @@ -78,12 +78,12 @@ def test_relative_description_key(relative_key: str, absolute_key: str) -> None: "extra_field": "extra_value", }, { - vol.Required("entity"): str, - vol.Optional("from"): str, - vol.Optional("to"): str, - vol.Optional("for"): dict, - vol.Optional("attribute"): str, - vol.Optional("value_template"): str, + probatio.Required("entity"): str, + probatio.Optional("from"): str, + probatio.Optional("to"): str, + probatio.Optional("for"): dict, + probatio.Optional("attribute"): str, + probatio.Optional("value_template"): str, }, { "platform": "test", @@ -176,7 +176,7 @@ async def test_move_schema_fields_to_options( ) async def test_move_options_fields_to_top_level(config, expected_config) -> None: """Test moving options fields to top-level.""" - base_schema = vol.Schema({vol.Required("platform"): str}) + base_schema = probatio.Schema({probatio.Required("platform"): str}) original_config = config.copy() assert move_options_fields_to_top_level(config, base_schema) == expected_config assert config == original_config # Ensure original config is not modified diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 16aa68ec182758..b1fdcbc2a22a29 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -3,8 +3,8 @@ import logging from unittest.mock import Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.config import YAML_CONFIG_FILE from homeassistant.core import HomeAssistant @@ -316,8 +316,10 @@ async def test_platform_schema_error( config: dict | None, ) -> None: """Test schema error in platform.""" - comp_platform_schema = cv.PLATFORM_SCHEMA.extend({vol.Remove("old"): str}) - comp_platform_schema_base = comp_platform_schema.extend({}, extra=vol.ALLOW_EXTRA) + comp_platform_schema = cv.PLATFORM_SCHEMA.extend({probatio.Remove("old"): str}) + comp_platform_schema_base = comp_platform_schema.extend( + {}, extra=probatio.ALLOW_EXTRA + ) mock_integration( hass, MockModule("blah", platform_schema_base=comp_platform_schema_base), diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index 86753adfd878a6..9d78b52020513d 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -4,8 +4,8 @@ import logging from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.helpers import ( @@ -522,8 +522,8 @@ async def test_storage_collection_websocket( coll, "test_item/collection", "test_item", - {vol.Required("name"): str, vol.Required("immutable_string"): str}, - {vol.Optional("name"): str}, + {probatio.Required("name"): str, probatio.Required("immutable_string"): str}, + {probatio.Optional("name"): str}, ).async_setup(hass) client = await hass_ws_client(hass) @@ -655,8 +655,8 @@ async def test_storage_collection_websocket_subscribe( coll, "test_item/collection", "test_item", - {vol.Required("name"): str, vol.Required("immutable_string"): str}, - {vol.Optional("name"): str}, + {probatio.Required("name"): str, probatio.Required("immutable_string"): str}, + {probatio.Optional("name"): str}, ).async_setup(hass) client = await hass_ws_client(hass) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 4789bc13e82a6c..f28dd77dc37c74 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -12,10 +12,10 @@ from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory +import probatio import pytest from pytest_unordered import unordered from sqlalchemy.exc import SQLAlchemyError -import voluptuous as vol from homeassistant.components.device_automation import ( DOMAIN as DEVICE_AUTOMATION_DOMAIN, @@ -175,7 +175,7 @@ def assert_condition_trace(expected): ) async def test_invalid_condition(hass: HomeAssistant, config: dict, error: str) -> None: """Test if validating an invalid condition raises.""" - with pytest.raises(vol.Invalid, match=error): + with pytest.raises(probatio.Invalid, match=error): cv.CONDITION_SCHEMA(config) @@ -538,7 +538,7 @@ async def test_malformed_and_condition_list_shorthand(hass: HomeAssistant) -> No "condition": ["bad", "syntax"], } - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): cv.CONDITION_SCHEMA(config) @@ -1518,7 +1518,7 @@ def test_state_for_not_allowed(extra_config: dict[str, Any], error: str) -> None "for": {"seconds": 5}, **extra_config, } - with pytest.raises(vol.Invalid, match=error): + with pytest.raises(probatio.Invalid, match=error): cv.CONDITION_SCHEMA(config) @@ -2548,7 +2548,7 @@ async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition] assert await async_validate_condition_config(hass, config_1) == config_1 assert await async_validate_condition_config(hass, config_2) == config_2 with pytest.raises( - vol.Invalid, match="Invalid condition 'test.unknown_cond' specified" + probatio.Invalid, match="Invalid condition 'test.unknown_cond' specified" ): await async_validate_condition_config(hass, config_3) @@ -2566,8 +2566,8 @@ async def test_platform_migrate_condition(hass: HomeAssistant) -> None: """Test a condition platform with a migration.""" OPTIONS_SCHEMA_DICT = { - vol.Required("option_1"): str, - vol.Optional("option_2"): int, + probatio.Required("option_1"): str, + probatio.Optional("option_2"): int, } class MockCondition(Condition): @@ -3683,7 +3683,7 @@ async def async_get_conditions( CONF_TARGET: {CONF_ENTITY_ID: "test.entity_1"}, CONF_OPTIONS: {}, } - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_condition_config(hass, config) @@ -3691,7 +3691,7 @@ async def async_get_conditions( ("above", "below", "expected_result"), [ (10.0, 10.0, does_not_raise()), - (20.0, 10.0, pytest.raises(vol.Invalid, match="must not be greater")), + (20.0, 10.0, pytest.raises(probatio.Invalid, match="must not be greater")), ], ) async def test_numerical_condition_schema_above_must_be_less_than_below( @@ -4143,7 +4143,7 @@ async def async_get_conditions( } }, } - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await async_validate_condition_config(hass, config) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index fce9e0dc324ef0..2d32ab2ea7428d 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -14,9 +14,9 @@ from unittest.mock import ANY, Mock, patch import uuid +import probatio import py import pytest -import voluptuous as vol import homeassistant from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant @@ -33,7 +33,7 @@ def test_boolean() -> None: """Test boolean validation.""" - schema = vol.Schema(cv.boolean) + schema = probatio.Schema(cv.boolean) for value in ( None, @@ -46,7 +46,7 @@ def test_boolean() -> None: {"one": "two"}, test_boolean, ): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("true", "On", "1", "YES", " true ", "enable", 1, 50, True, 0.1): @@ -58,10 +58,10 @@ def test_boolean() -> None: def test_latitude() -> None: """Test latitude validation.""" - schema = vol.Schema(cv.latitude) + schema = probatio.Schema(cv.latitude) for value in ("invalid", None, -91, 91, "-91", "91", "123.01A"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("-89", 89, "12.34"): @@ -70,10 +70,10 @@ def test_latitude() -> None: def test_longitude() -> None: """Test longitude validation.""" - schema = vol.Schema(cv.longitude) + schema = probatio.Schema(cv.longitude) for value in ("invalid", None, -181, 181, "-181", "181", "123.01A"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("-179", 179, "12.34"): @@ -82,10 +82,10 @@ def test_longitude() -> None: def test_port() -> None: """Test TCP/UDP network port.""" - schema = vol.Schema(cv.port) + schema = probatio.Schema(cv.port) for value in ("invalid", None, -1, 0, 80000, "81000"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("1000", 21, 24574): @@ -94,13 +94,13 @@ def test_port() -> None: def test_isfile() -> None: """Validate that the value is an existing file.""" - schema = vol.Schema(cv.isfile) + schema = probatio.Schema(cv.isfile) fake_file = "this-file-does-not.exist" assert not os.path.isfile(fake_file) for value in ("invalid", None, -1, 0, 80000, fake_file): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(value) # patching methods that allow us to fake a file existing @@ -114,7 +114,7 @@ def test_isfile() -> None: def test_url() -> None: """Test URL.""" - schema = vol.Schema(cv.url) + schema = probatio.Schema(cv.url) for value in ( "invalid", @@ -125,7 +125,7 @@ def test_url() -> None: "http://??,**", "https://??,**", ): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ( @@ -140,7 +140,7 @@ def test_url() -> None: def test_configuration_url() -> None: """Test URL.""" - schema = vol.Schema(cv.configuration_url) + schema = probatio.Schema(cv.configuration_url) for value in ( "invalid", @@ -152,7 +152,7 @@ def test_configuration_url() -> None: "https://??,**", "homeassistant://??,**", ): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ( @@ -169,14 +169,14 @@ def test_configuration_url() -> None: def test_url_no_path() -> None: """Test URL.""" - schema = vol.Schema(cv.url_no_path) + schema = probatio.Schema(cv.url_no_path) for value in ( "https://localhost/test/index.html", "http://home-assistant.io/test/", "http://invalid-port.local:999999", ): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ( @@ -191,7 +191,7 @@ def test_platform_config() -> None: """Test platform config validation.""" options = ({}, {"hello": "world"}) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): cv.PLATFORM_SCHEMA(value) options = ({"platform": "mqtt"}, {"platform": "mqtt", "beer": "yes"}) @@ -201,7 +201,7 @@ def test_platform_config() -> None: def test_ensure_list() -> None: """Test ensure_list.""" - schema = vol.Schema(cv.ensure_list) + schema = probatio.Schema(cv.ensure_list) assert schema(None) == [] assert schema(1) == [1] assert schema([1]) == [1] @@ -212,9 +212,9 @@ def test_ensure_list() -> None: def test_entity_id() -> None: """Test entity ID validation.""" - schema = vol.Schema(cv.entity_id) + schema = probatio.Schema(cv.entity_id) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema("invalid_entity") assert schema("sensor.LIGHT") == "sensor.light" @@ -223,7 +223,7 @@ def test_entity_id() -> None: @pytest.mark.parametrize("validator", [cv.entity_ids, cv.entity_ids_or_uuids]) def test_entity_ids(validator) -> None: """Test entity ID validation.""" - schema = vol.Schema(validator) + schema = probatio.Schema(validator) options = ( "invalid_entity", @@ -233,7 +233,7 @@ def test_entity_ids(validator) -> None: ["sensor.light,sensor_invalid"], ) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) options = ([], ["sensor.light"], "sensor.light") @@ -245,7 +245,7 @@ def test_entity_ids(validator) -> None: def test_entity_ids_or_uuids() -> None: """Test entity ID validation.""" - schema = vol.Schema(cv.entity_ids_or_uuids) + schema = probatio.Schema(cv.entity_ids_or_uuids) valid_uuid = "a266a680b608c32770e6c45bfe6b8411" valid_uuid2 = "a266a680b608c32770e6c45bfe6b8412" @@ -259,7 +259,7 @@ def test_entity_ids_or_uuids() -> None: [f"{valid_uuid},invalid_uuid"], ) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) options = ([], [valid_uuid], valid_uuid) @@ -271,7 +271,7 @@ def test_entity_ids_or_uuids() -> None: def test_entity_domain() -> None: """Test entity domain validation.""" - schema = vol.Schema(cv.entity_domain("sensor")) + schema = probatio.Schema(cv.entity_domain("sensor")) for value in ( "invalid_entity", @@ -279,15 +279,15 @@ def test_entity_domain() -> None: "cover.demo,sensor.another_entity", "", ): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) assert schema("sensor.LIGHT") == "sensor.light" - schema = vol.Schema(cv.entity_domain(("sensor", "binary_sensor"))) + schema = probatio.Schema(cv.entity_domain(("sensor", "binary_sensor"))) for value in ("invalid_entity", "cover.demo"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) assert schema("sensor.LIGHT") == "sensor.light" @@ -296,7 +296,7 @@ def test_entity_domain() -> None: def test_entities_domain() -> None: """Test entities domain validation.""" - schema = vol.Schema(cv.entities_domain("sensor")) + schema = probatio.Schema(cv.entities_domain("sensor")) options = ( None, @@ -307,7 +307,7 @@ def test_entities_domain() -> None: ) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) options = ("sensor.light", ["SENSOR.light"], ["sensor.light", "sensor.demo"]) @@ -320,7 +320,7 @@ def test_entities_domain() -> None: def test_ensure_list_csv() -> None: """Test ensure_list_csv.""" - schema = vol.Schema(cv.ensure_list_csv) + schema = probatio.Schema(cv.ensure_list_csv) options = (None, 12, [], ["string"], "string1,string2") for value in options: @@ -338,7 +338,7 @@ def test_event_schema() -> None: {"event": "state_changed", "event_data": 1}, ) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): cv.EVENT_SCHEMA(value) options = ( @@ -351,10 +351,10 @@ def test_event_schema() -> None: def test_icon() -> None: """Test icon validation.""" - schema = vol.Schema(cv.icon) + schema = probatio.Schema(cv.icon) for value in (False, "work"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) schema("mdi:work") @@ -363,7 +363,7 @@ def test_icon() -> None: def test_time_period() -> None: """Test time_period validation.""" - schema = vol.Schema(cv.time_period) + schema = probatio.Schema(cv.time_period) options = ( None, @@ -379,7 +379,7 @@ def test_time_period() -> None: "12:30.5:30", ) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) options = ( @@ -417,9 +417,9 @@ def test_remove_falsy() -> None: def test_service() -> None: """Test service validation.""" - schema = vol.Schema(cv.service) + schema = probatio.Schema(cv.service) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema("invalid_turn_on") schema("homeassistant.turn_on") @@ -503,14 +503,17 @@ def test_invalid_service_schema( hass: HomeAssistant, config: dict[str, Any] | None ) -> None: """Test service_schema validation fails.""" - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): cv.SERVICE_SCHEMA(config) def test_entity_service_schema() -> None: """Test make_entity_service_schema validation.""" schema = cv.make_entity_service_schema( - {vol.Required("required"): cv.positive_int, vol.Optional("optional"): cv.string} + { + probatio.Required("required"): cv.positive_int, + probatio.Optional("optional"): cv.string, + } ) options = ( @@ -523,7 +526,7 @@ def test_entity_service_schema() -> None: {"required": "str", "area_id": "kitchen"}, ) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): cv.SERVICE_SCHEMA(value) options = ( @@ -548,11 +551,13 @@ def test_entity_service_schema() -> None: def test_entity_service_schema_with_metadata() -> None: """Test make_entity_service_schema with overridden metadata key.""" - schema = cv.make_entity_service_schema({vol.Required("metadata"): cv.positive_int}) + schema = cv.make_entity_service_schema( + {probatio.Required("metadata"): cv.positive_int} + ) options = ({"metadata": {"some": "frontend_stuff"}, "entity_id": "light.kitchen"},) for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): cv.SERVICE_SCHEMA(value) options = ({"metadata": 1, "entity_id": "light.kitchen"},) @@ -563,10 +568,10 @@ def test_entity_service_schema_with_metadata() -> None: def test_slug() -> None: """Test slug validation.""" - schema = vol.Schema(cv.slug) + schema = probatio.Schema(cv.slug) for value in (None, "hello world"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in (12345, "hello"): @@ -575,15 +580,15 @@ def test_slug() -> None: def test_string(hass: HomeAssistant) -> None: """Test string validation.""" - schema = vol.Schema(cv.string) + schema = probatio.Schema(cv.string) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(None) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema([]) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema({}) for value in (True, 1, "hello"): @@ -612,15 +617,15 @@ class MyString(str): def test_string_with_no_html() -> None: """Test string with no html validation.""" - schema = vol.Schema(cv.string_with_no_html) + schema = probatio.Schema(cv.string_with_no_html) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("This has HTML in it Link") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("Bold") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("HTML element names are case-insensitive.") for value in ( @@ -635,9 +640,9 @@ def test_string_with_no_html() -> None: def test_temperature_unit() -> None: """Test temperature unit validation.""" - schema = vol.Schema(cv.temperature_unit) + schema = probatio.Schema(cv.temperature_unit) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema("K") schema("C") @@ -646,12 +651,12 @@ def test_temperature_unit() -> None: def test_x10_address() -> None: """Test x10 addr validator.""" - schema = vol.Schema(cv.x10_address) - with pytest.raises(vol.Invalid): + schema = probatio.Schema(cv.x10_address) + with pytest.raises(probatio.Invalid): schema("Q1") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("q55") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("garbage_addr") schema("a1") @@ -660,7 +665,7 @@ def test_x10_address() -> None: def test_template(hass: HomeAssistant) -> None: """Test template validator.""" - schema = vol.Schema(cv.template) + schema = probatio.Schema(cv.template) for value in ( None, @@ -668,7 +673,7 @@ def test_template(hass: HomeAssistant) -> None: "{% if True %}Hello", ["test"], ): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(value) options = ( @@ -689,7 +694,7 @@ def test_template(hass: HomeAssistant) -> None: async def test_template_no_hass(hass: HomeAssistant) -> None: """Test template validator.""" - schema = vol.Schema(cv.template) + schema = probatio.Schema(cv.template) for value in ( None, @@ -699,7 +704,7 @@ async def test_template_no_hass(hass: HomeAssistant) -> None: # Filter added as an extension by Home Assistant "{{ ['group.foo']|expand|map(attribute='entity_id')|list }}", ): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.async_add_executor_job(schema, value) options = ( @@ -715,14 +720,14 @@ async def test_template_no_hass(hass: HomeAssistant) -> None: ) for value in options: with pytest.raises( - vol.Invalid, match="Validates schema outside the event loop" + probatio.Invalid, match="Validates schema outside the event loop" ): await hass.async_add_executor_job(schema, value) def test_dynamic_template(hass: HomeAssistant) -> None: """Test dynamic template validator.""" - schema = vol.Schema(cv.dynamic_template) + schema = probatio.Schema(cv.dynamic_template) for value in ( None, @@ -732,7 +737,7 @@ def test_dynamic_template(hass: HomeAssistant) -> None: ["test"], "just a string", ): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(value) options = ( @@ -752,10 +757,10 @@ def test_dynamic_template(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("hass") def test_template_complex() -> None: """Test template_complex validator.""" - schema = vol.Schema(cv.template_complex) + schema = probatio.Schema(cv.template_complex) for value in ("{{ partial_print }", "{% if True %}Hello"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) options = ( @@ -786,9 +791,9 @@ def test_template_complex() -> None: def test_time_zone() -> None: """Test time zone validation.""" - schema = vol.Schema(cv.time_zone) + schema = probatio.Schema(cv.time_zone) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema("America/Do_Not_Exist") schema("America/Los_Angeles") @@ -797,10 +802,10 @@ def test_time_zone() -> None: def test_date() -> None: """Test date validation.""" - schema = vol.Schema(cv.date) + schema = probatio.Schema(cv.date) for value in ("Not a date", "23:42", "2016-11-23T18:59:08"): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(value) schema(dt_util.now().date()) @@ -809,10 +814,10 @@ def test_date() -> None: def test_time() -> None: """Test date validation.""" - schema = vol.Schema(cv.time) + schema = probatio.Schema(cv.time) for value in ("Not a time", "2016-11-23", "2016-11-23T18:59:08"): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(value) schema(dt_util.now().time()) @@ -822,9 +827,9 @@ def test_time() -> None: def test_datetime() -> None: """Test date time validation.""" - schema = vol.Schema(cv.datetime) + schema = probatio.Schema(cv.datetime) for value in (date.today(), "Wrong DateTime"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) schema(dt_util.now()) @@ -838,11 +843,11 @@ def test_multi_select() -> None: - Will not accept any input but a list - Will not accept selections outside of configured scope """ - schema = vol.Schema(cv.multi_select({"paulus": "Paulus", "robban": "Robban"})) + schema = probatio.Schema(cv.multi_select({"paulus": "Paulus", "robban": "Robban"})) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("robban") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(["paulus", "martinhj"]) schema(["robban", "paulus"]) @@ -892,7 +897,9 @@ def test_positive_time_period_dict_in_serializer() -> None: @pytest.fixture def schema(): """Create a schema used for testing deprecation.""" - return vol.Schema({"venus": cv.boolean, "mars": cv.boolean, "jupiter": cv.boolean}) + return probatio.Schema( + {"venus": cv.boolean, "mars": cv.boolean, "jupiter": cv.boolean} + ) @pytest.fixture @@ -909,7 +916,7 @@ def test_deprecated_with_no_optionals(caplog: pytest.LogCaptureFixture, schema) - Processes schema without changing any values - No warning or difference in output if key is not provided """ - deprecated_schema = vol.All(cv.deprecated("mars"), schema) + deprecated_schema = probatio.All(cv.deprecated("mars"), schema) test_data = {"mars": True} output = deprecated_schema(test_data.copy()) @@ -941,10 +948,10 @@ def test_deprecated_or_removed_param_and_raise( - Outputs the appropriate deprecation or removed from support error if key is detected """ - removed_schema = vol.All(cv.deprecated("mars", raise_if_present=True), schema) + removed_schema = probatio.All(cv.deprecated("mars", raise_if_present=True), schema) test_data = {"mars": True} - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: removed_schema(test_data) assert ( "The 'mars' option is deprecated, please remove it from your configuration" @@ -957,10 +964,10 @@ def test_deprecated_or_removed_param_and_raise( assert len(caplog.records) == 0 assert test_data == output - deprecated_schema = vol.All(cv.removed("mars"), schema) + deprecated_schema = probatio.All(cv.removed("mars"), schema) test_data = {"mars": True} - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: deprecated_schema(test_data) assert ( "The 'mars' option has been removed, please remove it from your configuration" @@ -987,7 +994,7 @@ def test_deprecated_with_replacement_key( - No warning or difference in output if neither key nor replacement_key are provided """ - deprecated_schema = vol.All( + deprecated_schema = probatio.All( cv.deprecated("mars", replacement_key="jupiter"), schema ) @@ -1021,7 +1028,7 @@ def test_deprecated_with_default(caplog: pytest.LogCaptureFixture, schema) -> No Expected behavior: - Behaves identically as when the default value was not present """ - deprecated_schema = vol.All(cv.deprecated("mars", default=False), schema) + deprecated_schema = probatio.All(cv.deprecated("mars", default=False), schema) test_data = {"mars": True} with patch( @@ -1058,7 +1065,7 @@ def test_deprecated_with_replacement_key_and_default( - No warning if neither key nor replacement_key are provided - Adds replacement_key with default value in this case """ - deprecated_schema = vol.All( + deprecated_schema = probatio.All( cv.deprecated("mars", replacement_key="jupiter", default=False), schema ) @@ -1083,12 +1090,12 @@ def test_deprecated_with_replacement_key_and_default( assert len(caplog.records) == 0 assert output == {"venus": True, "jupiter": False} - deprecated_schema_with_default = vol.All( - vol.Schema( + deprecated_schema_with_default = probatio.All( + probatio.Schema( { "venus": cv.boolean, - vol.Optional("mars", default=False): cv.boolean, - vol.Optional("jupiter", default=False): cv.boolean, + probatio.Optional("mars", default=False): cv.boolean, + probatio.Optional("jupiter", default=False): cv.boolean, } ), cv.deprecated("mars", replacement_key="jupiter", default=False), @@ -1227,11 +1234,11 @@ def test_deprecated_logger_without_config_attributes( def test_key_dependency() -> None: """Test key_dependency validator.""" - schema = vol.Schema(cv.key_dependency("beer", "soda")) + schema = probatio.Schema(cv.key_dependency("beer", "soda")) options = {"beer": None} for value in options: - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) options = ({"beer": None, "soda": None}, {"soda": None}, {}) @@ -1241,25 +1248,30 @@ def test_key_dependency() -> None: def test_has_at_most_one_key() -> None: """Test has_at_most_one_key validator.""" - schema = vol.Schema(cv.has_at_most_one_key("beer", "soda")) + schema = probatio.Schema(cv.has_at_most_one_key("beer", "soda")) for value in (None, [], {"beer": None, "soda": None}): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) - for value in ({}, {"beer": None}, {"soda": None}, {vol.Optional("soda"): None}): + for value in ( + {}, + {"beer": None}, + {"soda": None}, + {probatio.Optional("soda"): None}, + ): schema(value) def test_has_at_least_one_key() -> None: """Test has_at_least_one_key validator.""" - schema = vol.Schema(cv.has_at_least_one_key("beer", "soda")) + schema = probatio.Schema(cv.has_at_least_one_key("beer", "soda")) for value in (None, [], {}, {"wine": None}): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) - for value in ({"beer": None}, {"soda": None}, {vol.Required("soda"): None}): + for value in ({"beer": None}, {"soda": None}, {probatio.Required("soda"): None}): schema(value) @@ -1272,20 +1284,20 @@ class TestEnum(enum.Enum): value1 = "Value 1" value2 = "Value 2" - schema = vol.Schema(cv.enum(TestEnum)) + schema = probatio.Schema(cv.enum(TestEnum)) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("value3") def test_socket_timeout() -> None: """Test socket timeout validator.""" - schema = vol.Schema(cv.socket_timeout) + schema = probatio.Schema(cv.socket_timeout) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(0.0) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(-1) assert schema(None) == _GLOBAL_DEFAULT_TIMEOUT @@ -1295,12 +1307,12 @@ def test_socket_timeout() -> None: def test_matches_regex() -> None: """Test matches_regex validator.""" - schema = vol.Schema(cv.matches_regex(".*uiae.*")) + schema = probatio.Schema(cv.matches_regex(".*uiae.*")) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(1.0) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(" nrtd ") test_str = "This is a test including uiae." @@ -1309,12 +1321,12 @@ def test_matches_regex() -> None: def test_is_regex() -> None: """Test the is_regex validator.""" - schema = vol.Schema(cv.is_regex) + schema = probatio.Schema(cv.is_regex) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("(") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema({"a dict": "is not a regex"}) valid_re = ".*" @@ -1323,7 +1335,7 @@ def test_is_regex() -> None: def test_comp_entity_ids() -> None: """Test config validation for component entity IDs.""" - schema = vol.Schema(cv.comp_entity_ids) + schema = probatio.Schema(cv.comp_entity_ids) for valid in ( "ALL", @@ -1337,23 +1349,23 @@ def test_comp_entity_ids() -> None: schema(valid) for invalid in (["light.kitchen", "not-entity-id"], "*", ""): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(invalid) def test_uuid4_hex(caplog: pytest.LogCaptureFixture) -> None: """Test uuid validation.""" - schema = vol.Schema(cv.uuid4_hex) + schema = probatio.Schema(cv.uuid4_hex) for value in ("Not a hex string", "0", 0): - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema(value) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): # the 13th char should be 4 schema("a03d31b22eee1acc9b90eec40be6ed23") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): # the 17th char should be 8-a schema("a03d31b22eee4acc7b90eec40be6ed23") @@ -1364,33 +1376,33 @@ def test_uuid4_hex(caplog: pytest.LogCaptureFixture) -> None: def test_key_value_schemas() -> None: """Test key value schemas.""" - schema = vol.Schema( + schema = probatio.Schema( cv.key_value_schemas( "mode", { - "number": vol.Schema({"mode": "number", "data": int}), - "string": vol.Schema({"mode": "string", "data": str}), + "number": probatio.Schema({"mode": "number", "data": int}), + "string": probatio.Schema({"mode": "string", "data": str}), }, ) ) - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema(True) assert str(excinfo.value) == "Expected a dictionary" for mode in None, {"a": "dict"}, "invalid": - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema({"mode": mode}) assert ( str(excinfo.value) == f"Unexpected value for mode: '{mode}'. Expected number, string" ) - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema({"mode": "number", "data": "string-value"}) assert str(excinfo.value) == "expected int at 'data'" - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema({"mode": "string", "data": 1}) assert str(excinfo.value) == "expected str at 'data'" @@ -1401,35 +1413,35 @@ def test_key_value_schemas() -> None: @pytest.mark.usefixtures("hass") def test_key_value_schemas_with_default() -> None: """Test key value schemas.""" - schema = vol.Schema( + schema = probatio.Schema( cv.key_value_schemas( "mode", { - "number": vol.Schema({"mode": "number", "data": int}), - "string": vol.Schema({"mode": "string", "data": str}), + "number": probatio.Schema({"mode": "number", "data": int}), + "string": probatio.Schema({"mode": "string", "data": str}), }, - vol.Schema({"mode": cv.dynamic_template}), + probatio.Schema({"mode": cv.dynamic_template}), "a cool template", ) ) - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema(True) assert str(excinfo.value) == "Expected a dictionary" for mode in None, {"a": "dict"}, "invalid": - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema({"mode": mode}) assert ( str(excinfo.value) == f"Unexpected value for mode: '{mode}'." " Expected number, string, a cool template" ) - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema({"mode": "number", "data": "string-value"}) assert str(excinfo.value) == "expected int at 'data'" - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema({"mode": "string", "data": 1}) assert str(excinfo.value) == "expected str at 'data'" @@ -1441,25 +1453,25 @@ def test_key_value_schemas_with_default() -> None: @pytest.mark.usefixtures("hass") def test_key_value_schemas_with_default_no_list_alternatives() -> None: """Test key value schemas.""" - schema = vol.Schema( + schema = probatio.Schema( cv.key_value_schemas( "mode", { - "number": vol.Schema({"mode": "number", "data": int}), - "string": vol.Schema({"mode": "string", "data": str}), + "number": probatio.Schema({"mode": "number", "data": int}), + "string": probatio.Schema({"mode": "string", "data": str}), }, - vol.Schema({"mode": cv.dynamic_template}), + probatio.Schema({"mode": cv.dynamic_template}), "a cool template", list_alternatives=False, ) ) - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema(True) assert str(excinfo.value) == "Expected a dictionary" for mode in None, {"a": "dict"}, "invalid": - with pytest.raises(vol.Invalid) as excinfo: + with pytest.raises(probatio.Invalid) as excinfo: schema({"mode": mode}) assert ( str(excinfo.value) @@ -1471,14 +1483,14 @@ def test_key_value_schemas_with_default_no_list_alternatives() -> None: def test_key_value_schemas_without_default_no_list_alternatives() -> None: """Test key value schemas.""" with pytest.raises(ValueError) as excinfo: - vol.Schema( + probatio.Schema( cv.key_value_schemas( "mode", { - "number": vol.Schema({"mode": "number", "data": int}), - "string": vol.Schema({"mode": "string", "data": str}), + "number": probatio.Schema({"mode": "number", "data": int}), + "string": probatio.Schema({"mode": "string", "data": str}), }, - vol.Schema({"mode": cv.dynamic_template}), + probatio.Schema({"mode": cv.dynamic_template}), list_alternatives=False, ) ) @@ -1530,13 +1542,13 @@ def test_key_value_schemas_without_default_no_list_alternatives() -> None: @pytest.mark.usefixtures("hass") def test_script(caplog: pytest.LogCaptureFixture, config: dict, error: str) -> None: """Test script action validation is user friendly.""" - with pytest.raises(vol.Invalid, match=error): + with pytest.raises(probatio.Invalid, match=error): cv.script_action(config) def test_whitespace() -> None: """Test whitespace validation.""" - schema = vol.Schema(cv.whitespace) + schema = probatio.Schema(cv.whitespace) for value in ( None, @@ -1548,7 +1560,7 @@ def test_whitespace() -> None: [1, 2], {"one": "two"}, ): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in (" ", " "): @@ -1557,13 +1569,13 @@ def test_whitespace() -> None: def test_currency() -> None: """Test currency validator.""" - schema = vol.Schema(cv.currency) + schema = probatio.Schema(cv.currency) for value in ( None, "BTC", ): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("EUR", "USD"): @@ -1572,10 +1584,10 @@ def test_currency() -> None: def test_historic_currency() -> None: """Test historic currency validator.""" - schema = vol.Schema(cv.historic_currency) + schema = probatio.Schema(cv.historic_currency) for value in (None, "BTC", "EUR"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("DEM", "NLG"): @@ -1584,10 +1596,10 @@ def test_historic_currency() -> None: def test_country() -> None: """Test country validator.""" - schema = vol.Schema(cv.country) + schema = probatio.Schema(cv.country) for value in (None, "Candyland", "USA"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("NL", "SE"): @@ -1596,10 +1608,10 @@ def test_country() -> None: def test_language() -> None: """Test language validator.""" - schema = vol.Schema(cv.language) + schema = probatio.Schema(cv.language) for value in (None, "Klingon", "english"): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema(value) for value in ("en", "sv"): @@ -1609,15 +1621,15 @@ def test_language() -> None: @pytest.mark.usefixtures("hass") def test_positive_time_period_template() -> None: """Test positive time period template validation.""" - schema = vol.Schema(cv.positive_time_period_template) + schema = probatio.Schema(cv.positive_time_period_template) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema({}) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema({5: 5}) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema({"invalid": 5}) - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(probatio.MultipleInvalid): schema("invalid") # Time periods pass @@ -1732,13 +1744,13 @@ def test_platform_only_schema( def test_domain() -> None: """Test domain.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): cv.domain_key(5) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): cv.domain_key("") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): cv.domain_key("hue ") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): cv.domain_key("hue ") assert cv.domain_key("hue") == "hue" assert cv.domain_key("hue1") == "hue1" @@ -1754,19 +1766,19 @@ def test_color_hex() -> None: assert cv.color_hex("#000000") == "#000000" msg = r"Color should be in the format #RRGGBB" - with pytest.raises(vol.Invalid, match=msg): + with pytest.raises(probatio.Invalid, match=msg): cv.color_hex("#777") - with pytest.raises(vol.Invalid, match=msg): + with pytest.raises(probatio.Invalid, match=msg): cv.color_hex("FFFFF") - with pytest.raises(vol.Invalid, match=msg): + with pytest.raises(probatio.Invalid, match=msg): cv.color_hex("FFFFFF") - with pytest.raises(vol.Invalid, match=msg): + with pytest.raises(probatio.Invalid, match=msg): cv.color_hex("#FFFFFFF") - with pytest.raises(vol.Invalid, match=msg): + with pytest.raises(probatio.Invalid, match=msg): cv.color_hex(123456) @@ -1818,14 +1830,14 @@ def _mock_validator_schema(real_func, *args): validator_calls = {} # Assert validation in executor when decorated with not_async_friendly - await cv.async_validate(hass, vol.All(cv.isdir, cv.string), tmpdir) + await cv.async_validate(hass, probatio.All(cv.isdir, cv.string), tmpdir) assert validator_calls == {"isdir": [hass.loop_thread_id, ANY], "string": [ANY]} assert validator_calls["isdir"][1] != hass.loop_thread_id assert validator_calls["string"][0] != hass.loop_thread_id validator_calls = {} # Assert validation in executor when decorated with not_async_friendly - await cv.async_validate(hass, vol.All(cv.string, cv.isdir), tmpdir) + await cv.async_validate(hass, probatio.All(cv.string, cv.isdir), tmpdir) assert validator_calls == { "isdir": [hass.loop_thread_id, ANY], "string": [hass.loop_thread_id, ANY], @@ -1840,7 +1852,7 @@ def _mock_validator_schema(real_func, *args): validator_calls = {} # Assert validation in event loop when not using cv.async_validate - vol.All(cv.isdir, cv.string)(tmpdir) + probatio.All(cv.isdir, cv.string)(tmpdir) assert validator_calls == { "isdir": [hass.loop_thread_id], "string": [hass.loop_thread_id], @@ -1848,7 +1860,7 @@ def _mock_validator_schema(real_func, *args): validator_calls = {} # Assert validation in event loop when not using cv.async_validate - vol.All(cv.string, cv.isdir)(tmpdir) + probatio.All(cv.string, cv.isdir)(tmpdir) assert validator_calls == { "isdir": [hass.loop_thread_id], "string": [hass.loop_thread_id], @@ -1953,14 +1965,14 @@ async def test_trigger_backwards_compatibility() -> None: assert cv._trigger_pre_validator({"platform": "abc"}) == {"platform": "abc"} assert cv._trigger_pre_validator({"trigger": "abc"}) == {"platform": "abc"} with pytest.raises( - vol.Invalid, + probatio.Invalid, match=( "Cannot specify both 'platform' and 'trigger'. Please use 'trigger' only." ), ): cv._trigger_pre_validator({"trigger": "abc", "platform": "def"}) with pytest.raises( - vol.Invalid, + probatio.Invalid, match=re.escape("required key not provided at 'trigger'"), ): cv._trigger_pre_validator({}) @@ -1971,28 +1983,28 @@ async def test_is_entity_service_schema( ) -> None: """Test cv.is_entity_service_schema.""" for schema in ( - vol.Schema({"some": str}), - vol.All(vol.Schema({"some": str})), - vol.Any(vol.Schema({"some": str})), - vol.Any(cv.make_entity_service_schema({"some": str})), + probatio.Schema({"some": str}), + probatio.All(probatio.Schema({"some": str})), + probatio.Any(probatio.Schema({"some": str})), + probatio.Any(cv.make_entity_service_schema({"some": str})), ): assert cv.is_entity_service_schema(schema) is False for schema in ( cv.make_entity_service_schema({"some": str}), - vol.Schema(cv.make_entity_service_schema({"some": str})), - vol.Schema(vol.All(cv.make_entity_service_schema({"some": str}))), - vol.Schema(vol.Schema(cv.make_entity_service_schema({"some": str}))), - vol.All(cv.make_entity_service_schema({"some": str})), - vol.All(vol.All(cv.make_entity_service_schema({"some": str}))), - vol.All(vol.Schema(cv.make_entity_service_schema({"some": str}))), + probatio.Schema(cv.make_entity_service_schema({"some": str})), + probatio.Schema(probatio.All(cv.make_entity_service_schema({"some": str}))), + probatio.Schema(probatio.Schema(cv.make_entity_service_schema({"some": str}))), + probatio.All(cv.make_entity_service_schema({"some": str})), + probatio.All(probatio.All(cv.make_entity_service_schema({"some": str}))), + probatio.All(probatio.Schema(cv.make_entity_service_schema({"some": str}))), ): assert cv.is_entity_service_schema(schema) is True def test_renamed(caplog: pytest.LogCaptureFixture, schema) -> None: """Test renamed.""" - renamed_schema = vol.All(cv.renamed("mors", "mars"), schema) + renamed_schema = probatio.All(cv.renamed("mors", "mars"), schema) test_data = {"mars": True} output = renamed_schema(test_data.copy()) @@ -2006,14 +2018,14 @@ def test_renamed(caplog: pytest.LogCaptureFixture, schema) -> None: test_data = {"mars": True, "mors": True} with pytest.raises( - vol.Invalid, + probatio.Invalid, match="Cannot specify both 'mors' and 'mars'. Please use 'mars' only.", ): renamed_schema(test_data.copy()) assert len(caplog.records) == 0 # Check error handling if data is not a dict - with pytest.raises(vol.Invalid, match="expected a mapping"): + with pytest.raises(probatio.Invalid, match="expected a mapping"): renamed_schema([]) @@ -2022,7 +2034,7 @@ def test_stop_action_schema_error_false_with_response() -> None: schema = cv._SCRIPT_STOP_SCHEMA # error: true with response_variable should fail - with pytest.raises(vol.Invalid, match="not allowed to add a response"): + with pytest.raises(probatio.Invalid, match="not allowed to add a response"): schema({"stop": "Error", "error": True, "response_variable": "result"}) # error: false with response_variable should work @@ -2083,7 +2095,7 @@ def test_base_schemas_reject_invalid_note( invalid_note: Any, ) -> None: """Test that script, condition, trigger base schemas reject non-string notes.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): validator({**base_config, "note": invalid_note}) @@ -2117,7 +2129,7 @@ def test_choose_option_accepts_note() -> None: @pytest.mark.usefixtures("hass") def test_choose_option_rejects_invalid_note(invalid_note: Any) -> None: """Test that choose option schemas reject non-string notes.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): cv.script_action( {"choose": [{**_CHOOSE_OPTION_BASE_CONFIG, "note": invalid_note}]} ) diff --git a/tests/helpers/test_device.py b/tests/helpers/test_device.py index c5cc48484b94ce..dbe1812c88e5ee 100644 --- a/tests/helpers/test_device.py +++ b/tests/helpers/test_device.py @@ -2,8 +2,8 @@ from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -87,13 +87,13 @@ async def test_entity_id_to_device_device_id( == device ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): async_entity_id_to_device_id( hass, entity_id_or_uuid="unknown_uuid", ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): async_entity_id_to_device( hass, entity_id_or_uuid="unknown_uuid", diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 193e6ace90e439..3b8b530378cb19 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -10,11 +10,11 @@ from unittest.mock import MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory +import probatio from propcache.api import cached_property import pytest from pytest_unordered import unordered from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigSubentryData from homeassistant.const import ( @@ -979,7 +979,7 @@ async def test_entity_category_property(hass: HomeAssistant) -> None: ) def test_entity_category_schema(value, expected) -> None: """Test entity category schema.""" - schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) + schema = probatio.Schema(entity.ENTITY_CATEGORIES_SCHEMA) result = schema(value) assert result == expected assert isinstance(result, EntityCategory) @@ -988,9 +988,9 @@ def test_entity_category_schema(value, expected) -> None: @pytest.mark.parametrize("value", [None, "non_existing"]) def test_entity_category_schema_error(value) -> None: """Test entity category schema.""" - schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) + schema = probatio.Schema(entity.ENTITY_CATEGORIES_SCHEMA) with pytest.raises( - vol.Invalid, + probatio.Invalid, match=r"expected EntityCategory or one of 'config', 'diagnostic'", ): schema(value) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 64efde060592dc..26671d456f90e9 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -7,9 +7,9 @@ from unittest.mock import AsyncMock, Mock, patch from freezegun import freeze_time +import probatio import pytest from pytest_unordered import unordered -import voluptuous as vol from homeassistant.const import ( ENTITY_MATCH_ALL, @@ -553,7 +553,7 @@ def appender(**kwargs): "test_placeholder": "beer" } - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "hello", @@ -761,7 +761,7 @@ async def handle_service(entities: list[MockEntity], call: ServiceCall) -> None: "test_placeholder": "beer" } - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( DOMAIN, "hello", @@ -851,9 +851,9 @@ async def test_register_entity_service_non_entity_service_schema( for idx, schema in enumerate( ( - vol.Schema({"some": str}), - vol.All(vol.Schema({"some": str})), - vol.Any(vol.Schema({"some": str})), + probatio.Schema({"some": str}), + probatio.All(probatio.Schema({"some": str})), + probatio.Any(probatio.Schema({"some": str})), ) ): expected_message = ( @@ -870,8 +870,8 @@ async def test_register_entity_service_non_entity_service_schema( for idx, schema in enumerate( ( cv.make_entity_service_schema({"some": str}), - vol.Schema(cv.make_entity_service_schema({"some": str})), - vol.All(cv.make_entity_service_schema({"some": str})), + probatio.Schema(cv.make_entity_service_schema({"some": str})), + probatio.All(cv.make_entity_service_schema({"some": str})), ) ): component.async_register_entity_service(f"test_service_{idx}", schema, Mock()) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 1414de00e2c249..97270ea8b05a01 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -8,9 +8,9 @@ from typing import Any from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigSubentryData from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE, EntityCategory @@ -2192,9 +2192,9 @@ async def test_register_entity_service_non_entity_service_schema( for idx, schema in enumerate( ( - vol.Schema({"some": str}), - vol.All(vol.Schema({"some": str})), - vol.Any(vol.Schema({"some": str})), + probatio.Schema({"some": str}), + probatio.All(probatio.Schema({"some": str})), + probatio.Any(probatio.Schema({"some": str})), ) ): expected_message = ( @@ -2209,8 +2209,8 @@ async def test_register_entity_service_non_entity_service_schema( for idx, schema in enumerate( ( cv.make_entity_service_schema({"some": str}), - vol.Schema(cv.make_entity_service_schema({"some": str})), - vol.All(cv.make_entity_service_schema({"some": str})), + probatio.Schema(cv.make_entity_service_schema({"some": str})), + probatio.All(cv.make_entity_service_schema({"some": str})), ) ): entity_platform.async_register_entity_service( diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 0323d35c755e85..fba95d940611c1 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -8,8 +8,8 @@ import attr from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( @@ -4201,13 +4201,13 @@ async def test_resolve_entity_ids(entity_registry: er.EntityRegistry) -> None: == expected ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): er.async_validate_entity_ids(entity_registry, ["light.beer", "bad_uuid"]) expected = ["light.unknown"] assert er.async_validate_entity_ids(entity_registry, ["light.unknown"]) == expected - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): er.async_validate_entity_ids(entity_registry, ["unknown_uuid"]) diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index f9a994cd378b4d..c164e939cc69bc 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -4,8 +4,8 @@ from copy import deepcopy from unittest.mock import MagicMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant.components import light, switch from homeassistant.components.homeassistant.exposed_entities import async_expose_entity @@ -653,13 +653,13 @@ async def test_match_child_device_area( def test_async_validate_slots() -> None: """Test async_validate_slots of IntentHandler.""" - handler1 = MockIntentHandler({vol.Required("name"): cv.string}) + handler1 = MockIntentHandler({probatio.Required("name"): cv.string}) - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): handler1.async_validate_slots({}) - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): handler1.async_validate_slots({"name": 1}) - with pytest.raises(vol.error.MultipleInvalid): + with pytest.raises(probatio.error.MultipleInvalid): handler1.async_validate_slots({"name": "kitchen"}) handler1.async_validate_slots({"name": {"value": "kitchen"}}) handler1.async_validate_slots( diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 3eface1a095181..9dcf61bb5340a8 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -3,8 +3,8 @@ from decimal import Decimal from unittest.mock import patch +import probatio import pytest -import voluptuous as vol from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.intent import async_register_timer_handler @@ -161,10 +161,10 @@ async def test_assist_api( device_id=None, ) schema = { - vol.Optional("area"): cv.string, - vol.Optional("floor"): cv.string, - vol.Optional("preferred_area_id"): cv.string, - vol.Optional("preferred_floor_id"): cv.string, + probatio.Optional("area"): cv.string, + probatio.Optional("floor"): cv.string, + probatio.Optional("preferred_area_id"): cv.string, + probatio.Optional("preferred_floor_id"): cv.string, } class MyIntentHandler(intent.IntentHandler): @@ -176,10 +176,10 @@ class MyIntentHandler(intent.IntentHandler): tool = llm.IntentTool("test_intent", intent_handler) assert tool.name == "test_intent" assert tool.description == "Execute Home Assistant test_intent intent" - assert tool.parameters == vol.Schema( + assert tool.parameters == probatio.Schema( { - vol.Optional("area"): cv.string, - vol.Optional("floor"): cv.string, + probatio.Optional("area"): cv.string, + probatio.Optional("floor"): cv.string, # No preferred_area_id, preferred_floor_id } ) @@ -851,20 +851,20 @@ async def test_action_tool( == "This is a test script. Aliases: ['script alias', 'script name']" ) schema = { - vol.Required("beer", description="Number of beers"): cv.string, - vol.Optional("wine"): selector.NumberSelector({"min": 0, "max": 3}), - vol.Optional("where"): selector.AreaSelector(), - vol.Optional("area_list"): selector.AreaSelector({"multiple": True}), - vol.Optional("floor"): selector.FloorSelector(), - vol.Optional("floor_list"): selector.FloorSelector({"multiple": True}), - vol.Optional("extra_field"): selector.AreaSelector(), + probatio.Required("beer", description="Number of beers"): cv.string, + probatio.Optional("wine"): selector.NumberSelector({"min": 0, "max": 3}), + probatio.Optional("where"): selector.AreaSelector(), + probatio.Optional("area_list"): selector.AreaSelector({"multiple": True}), + probatio.Optional("floor"): selector.FloorSelector(), + probatio.Optional("floor_list"): selector.FloorSelector({"multiple": True}), + probatio.Optional("extra_field"): selector.AreaSelector(), } assert tool.parameters.schema == schema # The parameter cache stores the base description; ScriptTool appends aliases. assert hass.data[llm.ACTION_PARAMETERS_CACHE]["script"] == { - "test_script": ("This is a test script", vol.Schema(schema)), - "script_with_no_fields": ("This is another test script", vol.Schema({})), + "test_script": ("This is a test script", probatio.Schema(schema)), + "script_with_no_fields": ("This is another test script", probatio.Schema({})), } # Test script with response @@ -969,12 +969,12 @@ async def test_action_tool( tool.description == "This is a new test script. Aliases: ['script alias', 'script name']" ) - schema = {vol.Required("beer", description="Number of beers"): cv.string} + schema = {probatio.Required("beer", description="Number of beers"): cv.string} assert tool.parameters.schema == schema assert hass.data[llm.ACTION_PARAMETERS_CACHE]["script"] == { - "test_script": ("This is a new test script", vol.Schema(schema)), - "script_with_no_fields": ("This is another test script", vol.Schema({})), + "test_script": ("This is a new test script", probatio.Schema(schema)), + "script_with_no_fields": ("This is another test script", probatio.Schema({})), } diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index df7c48d7764470..4e924a9b574b87 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -3,8 +3,8 @@ import logging from unittest.mock import AsyncMock, Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant import config from homeassistant.const import SERVICE_RELOAD @@ -237,7 +237,7 @@ async def test_async_integration_failing_yaml_config(hass: HomeAssistant) -> Non In case an integration reloads its yaml configuration it should throw when the new config failed to load and raise_on_failure is set to True. """ - schema_without_name_attr = vol.Schema({vol.Required("some_option"): str}) + schema_without_name_attr = probatio.Schema({probatio.Required("some_option"): str}) mock_integration(hass, MockModule(DOMAIN, config_schema=schema_without_name_attr)) diff --git a/tests/helpers/test_schema_config_entry_flow.py b/tests/helpers/test_schema_config_entry_flow.py index 678689077d80ad..8bf9953ea3a81b 100644 --- a/tests/helpers/test_schema_config_entry_flow.py +++ b/tests/helpers/test_schema_config_entry_flow.py @@ -4,8 +4,8 @@ from typing import Any from unittest.mock import AsyncMock, patch +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.core import HomeAssistant, callback @@ -117,10 +117,12 @@ async def _option1_next_step(_: dict[str, Any]) -> str: CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { "user": SchemaFlowMenuStep(MENU_1), - "option1": SchemaFlowFormStep(vol.Schema({}), next_step=_option1_next_step), + "option1": SchemaFlowFormStep( + probatio.Schema({}), next_step=_option1_next_step + ), "menu2": SchemaFlowMenuStep(menu_2), - "option3": SchemaFlowFormStep(vol.Schema({}), next_step="option4"), - "option4": SchemaFlowFormStep(vol.Schema({})), + "option3": SchemaFlowFormStep(probatio.Schema({}), next_step="option4"), + "option4": SchemaFlowFormStep(probatio.Schema({})), } class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): @@ -167,9 +169,9 @@ async def test_schema_none(hass: HomeAssistant) -> None: CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { "user": SchemaFlowFormStep(next_step="option1"), - "option1": SchemaFlowFormStep(vol.Schema({}), next_step="pass"), + "option1": SchemaFlowFormStep(probatio.Schema({}), next_step="pass"), "pass": SchemaFlowFormStep(next_step="option3"), - "option3": SchemaFlowFormStep(vol.Schema({})), + "option3": SchemaFlowFormStep(probatio.Schema({})), } class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): @@ -201,9 +203,9 @@ async def _step2_next_step(_: dict[str, Any]) -> str: CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { "user": SchemaFlowFormStep(next_step="step1"), - "step1": SchemaFlowFormStep(vol.Schema({}), next_step="step2"), - "step2": SchemaFlowFormStep(vol.Schema({}), next_step=_step2_next_step), - "step3": SchemaFlowFormStep(vol.Schema({}), next_step=None), + "step1": SchemaFlowFormStep(probatio.Schema({}), next_step="step2"), + "step2": SchemaFlowFormStep(probatio.Schema({}), next_step=_step2_next_step), + "step3": SchemaFlowFormStep(probatio.Schema({}), next_step=None), } class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): @@ -245,8 +247,8 @@ async def _step2_next_step(_: dict[str, Any]) -> None: CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { "user": SchemaFlowFormStep(next_step="step1"), - "step1": SchemaFlowFormStep(vol.Schema({}), next_step=_step1_next_step), - "step2": SchemaFlowFormStep(vol.Schema({}), next_step=_step2_next_step), + "step1": SchemaFlowFormStep(probatio.Schema({}), next_step=_step1_next_step), + "step2": SchemaFlowFormStep(probatio.Schema({}), next_step=_step2_next_step), } class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): @@ -276,8 +278,8 @@ async def test_suggested_values( """Test suggested_values handling in SchemaFlowFormStep.""" manager.hass = hass - OPTIONS_SCHEMA = vol.Schema( - {vol.Optional("option1", default="a very reasonable default"): str} + OPTIONS_SCHEMA = probatio.Schema( + {probatio.Optional("option1", default="a very reasonable default"): str} ) async def _validate_user_input( @@ -323,7 +325,7 @@ class TestFlow(MockSchemaConfigFlowHandler, domain="test"): result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" - schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + schema_keys: list[probatio.Optional] = list(result["data_schema"].schema.keys()) assert schema_keys == ["option1"] assert schema_keys[0].description == {"suggested_value": "initial value"} @@ -333,7 +335,7 @@ class TestFlow(MockSchemaConfigFlowHandler, domain="test"): ) assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "step_1" - schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + schema_keys: list[probatio.Optional] = list(result["data_schema"].schema.keys()) assert schema_keys == ["option1"] assert schema_keys[0].description == {"suggested_value": "blublu"} @@ -343,7 +345,7 @@ class TestFlow(MockSchemaConfigFlowHandler, domain="test"): ) assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "step_2" - schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + schema_keys: list[probatio.Optional] = list(result["data_schema"].schema.keys()) assert schema_keys == ["option1"] assert schema_keys[0].description == {"suggested_value": "a random override"} @@ -353,7 +355,7 @@ class TestFlow(MockSchemaConfigFlowHandler, domain="test"): ) assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "step_3" - schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + schema_keys: list[probatio.Optional] = list(result["data_schema"].schema.keys()) assert schema_keys == ["option1"] assert schema_keys[0].description is None @@ -363,7 +365,7 @@ class TestFlow(MockSchemaConfigFlowHandler, domain="test"): ) assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "step_4" - schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + schema_keys: list[probatio.Optional] = list(result["data_schema"].schema.keys()) assert schema_keys == ["option1"] assert schema_keys[0].description == {"suggested_value": "blabla"} @@ -373,7 +375,7 @@ class TestFlow(MockSchemaConfigFlowHandler, domain="test"): ) assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "step_4" - schema_keys: list[vol.Optional] = list(result["data_schema"].schema.keys()) + schema_keys: list[probatio.Optional] = list(result["data_schema"].schema.keys()) assert schema_keys == ["option1"] assert schema_keys[0].description == {"suggested_value": "not a valid value"} @@ -390,8 +392,8 @@ async def test_description_placeholders( """Test description_placeholders handling in SchemaFlowFormStep.""" manager.hass = hass - OPTIONS_SCHEMA = vol.Schema( - {vol.Optional("option1", default="a very reasonable default"): str} + OPTIONS_SCHEMA = probatio.Schema( + {probatio.Optional("option1", default="a very reasonable default"): str} ) async def _get_description_placeholders( @@ -426,8 +428,8 @@ class TestFlow(MockSchemaConfigFlowHandler, domain="test"): async def test_options_flow_state(hass: HomeAssistant) -> None: """Test flow_state handling in SchemaFlowFormStep.""" - OPTIONS_SCHEMA = vol.Schema( - {vol.Optional("option1", default="a very reasonable default"): str} + OPTIONS_SCHEMA = probatio.Schema( + {probatio.Optional("option1", default="a very reasonable default"): str} ) async def _init_schema(handler: SchemaCommonFlowHandler) -> None: @@ -511,10 +513,12 @@ async def test_options_flow_omit_optional_keys( """Test handling of optional keys in options flow.""" manager.hass = hass - OPTIONS_SCHEMA = vol.Schema( + OPTIONS_SCHEMA = probatio.Schema( { - vol.Optional("optional_no_default"): str, - vol.Optional("optional_default", default="a very reasonable default"): str, + probatio.Optional("optional_no_default"): str, + probatio.Optional( + "optional_default", default="a very reasonable default" + ): str, } ) @@ -575,7 +579,7 @@ async def test_options_flow_with_automatic_reload( """Test using options flow with automatic reloading.""" manager.hass = hass - OPTIONS_SCHEMA = vol.Schema({vol.Optional("some_string"): str}) + OPTIONS_SCHEMA = probatio.Schema({probatio.Optional("some_string"): str}) OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { "init": SchemaFlowFormStep(OPTIONS_SCHEMA) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b7c12148f81172..3a12eb142e06ca 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -12,8 +12,8 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch from freezegun import freeze_time +import probatio import pytest -import voluptuous as vol # Otherwise can't test just this file (import order issue) from homeassistant import config_entries, exceptions @@ -453,7 +453,7 @@ def mock_service(call: ServiceCall) -> ServiceResponse: ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") - with pytest.raises(vol.Invalid, match=expected_error): + with pytest.raises(probatio.Invalid, match=expected_error): await script_obj.async_run(context=context) @@ -4129,13 +4129,13 @@ async def test_propagate_error_invalid_service_data(hass: HomeAssistant) -> None """Test that a script aborts when we send invalid service data.""" event = "test_event" events = async_capture_events(hass, event) - calls = async_mock_service(hass, "test", "script", vol.Schema({"text": str})) + calls = async_mock_service(hass, "test", "script", probatio.Schema({"text": str})) sequence = cv.SCRIPT_SCHEMA( [{"action": "test.script", "data": {"text": 1}}, {"event": event}] ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await script_obj.async_run(context=Context()) assert len(events) == 0 @@ -5991,7 +5991,7 @@ def templated_device_action(message): validated_config[action_type] = await script.async_validate_action_config( hass, validated_config[action_type] ) - except vol.Invalid as err: + except probatio.Invalid as err: pytest.fail(f"{action_type} config invalid: {err}") # Verify non-static actions have validated diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index de846a75496f65..188871d07e47c7 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -5,9 +5,9 @@ from enum import Enum from typing import Any +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.helpers import selector from homeassistant.util import yaml as yaml_util @@ -40,7 +40,7 @@ def test_valid_base_schema(schema) -> None: ) def test_invalid_base_schema(schema) -> None: """Test base schema validation.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector(schema) @@ -93,14 +93,14 @@ def default_converter(x): assert not any(isinstance(val, Enum) for val in selector_instance.config.values()) # Use selector in schema and validate - vol_schema = vol.Schema({"selection": selector_instance}) + validation_schema = probatio.Schema({"selection": selector_instance}) for selection in valid_selections: - assert vol_schema({"selection": selection}) == { + assert validation_schema({"selection": selection}) == { "selection": converter(selection) } for selection in invalid_selections: - with pytest.raises(vol.Invalid): - vol_schema({"selection": selection}) + with pytest.raises(probatio.Invalid): + validation_schema({"selection": selection}) # Serialize selector selector_instance = selector.selector({selector_type: schema}) @@ -200,7 +200,7 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) -> ) def test_device_selector_schema_error(schema) -> None: """Test device selector.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"device": schema}) @@ -408,7 +408,7 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections) -> ) def test_entity_selector_schema_error(schema) -> None: """Test entity selector.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"entity": schema}) @@ -489,7 +489,7 @@ def test_area_selector_schema(schema, valid_selections, invalid_selections) -> N ) def test_area_selector_schema_error(schema) -> None: """Test area selector.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"area": schema}) @@ -573,7 +573,7 @@ def test_number_selector_schema_default_mode() -> None: ) def test_number_selector_schema_error(schema) -> None: """Test number selector.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"number": schema}) @@ -710,11 +710,11 @@ def test_numeric_threshold_selector_schema( def test_numeric_threshold_selector_invalid_config() -> None: """Test numeric threshold selector rejects an invalid or missing mode in config.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"numeric_threshold": {"mode": "invalid_mode"}}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"numeric_threshold": {}}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"numeric_threshold": None}) @@ -798,10 +798,10 @@ def test_numeric_threshold_selector_active_choice_extraction( value_in: Any, value_out: Any ) -> None: """Test that active_choice is stripped and only the active field is kept.""" - vol_schema = vol.Schema( + validation_schema = probatio.Schema( {"selection": selector.selector({"numeric_threshold": {"mode": "changed"}})} ) - assert vol_schema({"selection": value_in}) == {"selection": value_out} + assert validation_schema({"selection": value_in}) == {"selection": value_out} @pytest.mark.parametrize( @@ -948,7 +948,7 @@ def get_selected_value(data): # Invalid schemas ( {}, # Missing required 'choices' key - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( { @@ -962,7 +962,7 @@ def get_selected_value(data): "text": {} # Missing required 'selector' key in choice } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( { @@ -970,13 +970,13 @@ def get_selected_value(data): "invalid": {"selector": {"not_exist": {}}} # Invalid selector type } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( { "choices": "not a dict" # choices should be a dict }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( { @@ -993,7 +993,7 @@ def get_selected_value(data): } # Nested choose is not allowed } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) @@ -1319,7 +1319,7 @@ def test_automation_behavior_selector_schema( ) def test_automation_behavior_selector_schema_error(schema) -> None: """Test automation behavior selector config schema errors.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"automation_behavior": schema}) @@ -1562,9 +1562,9 @@ def test_nested_object_selectors(snapshot: SnapshotAssertion) -> None: "label_field": "name", "description_field": "percentage", }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), - ({"multiple": "False"}, pytest.raises(vol.Invalid)), + ({"multiple": "False"}, pytest.raises(probatio.Invalid)), ], ) def test_object_selector_validate_schema( @@ -1687,7 +1687,7 @@ def test_select_selector_schema(schema, valid_selections, invalid_selections) -> ) def test_select_selector_schema_error(schema) -> None: """Test select selector.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"select": schema}) @@ -1733,9 +1733,9 @@ def test_device_class_selector_schema( ({"state_classes": "total"}, does_not_raise()), ({"state_classes": ["total"]}, does_not_raise()), ({"state_classes": ["total", "measurement"]}, does_not_raise()), - ({"state_classes": ["cat"]}, pytest.raises(vol.Invalid)), - ({"state_classes": ["total", "beer"]}, pytest.raises(vol.Invalid)), - ({"state_classes": ["cat", "total"]}, pytest.raises(vol.Invalid)), + ({"state_classes": ["cat"]}, pytest.raises(probatio.Invalid)), + ({"state_classes": ["total", "beer"]}, pytest.raises(probatio.Invalid)), + ({"state_classes": ["cat", "total"]}, pytest.raises(probatio.Invalid)), ], ) def test_state_class_selector_validate_schema( @@ -2251,7 +2251,7 @@ def test_constant_selector_schema(schema, valid_selections, invalid_selections) ) def test_constant_selector_schema_error(schema) -> None: """Test constant selector.""" - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): selector.validate_selector({"constant": schema}) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index f9c4ecdc457299..df97cbf98433ea 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -8,9 +8,9 @@ from typing import Any from unittest.mock import AsyncMock, Mock, call as mock_call, patch +import probatio import pytest from pytest_unordered import unordered -import voluptuous as vol # To prevent circular import when running just this file from homeassistant import config_entries, exceptions @@ -1442,7 +1442,7 @@ async def test_async_get_descriptions_with_placeholders(hass: HomeAssistant) -> unit_of_measurement: "seconds" """ - service_schema = vol.Schema( + service_schema = probatio.Schema( { "topic": cv.string, "duration": cv.positive_int, @@ -1974,7 +1974,7 @@ async def mock_service(call): "test", "test2", mock_service, - vol.Schema({vol.Required("required"): cv.boolean}), + probatio.Schema({probatio.Required("required"): cv.boolean}), ) with pytest.raises(exceptions.UnknownUser): @@ -1997,7 +1997,7 @@ async def mock_service(call): ) assert len(calls) == 0 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( "test", "test", @@ -2007,7 +2007,7 @@ async def mock_service(call): ) assert len(calls) == 0 - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): await hass.services.async_call( "test", "test2", @@ -3322,9 +3322,9 @@ async def test_register_platform_entity_service_non_entity_service_schema( for idx, schema in enumerate( ( - vol.Schema({"some": str}), - vol.All(vol.Schema({"some": str})), - vol.Any(vol.Schema({"some": str})), + probatio.Schema({"some": str}), + probatio.All(probatio.Schema({"some": str})), + probatio.Any(probatio.Schema({"some": str})), ) ): with pytest.raises(HomeAssistantError, match=expected_message): @@ -3349,8 +3349,8 @@ async def test_register_platform_entity_service_non_entity_service_schema( for idx, schema in enumerate( ( cv.make_entity_service_schema({"some": str}), - vol.Schema(cv.make_entity_service_schema({"some": str})), - vol.All(cv.make_entity_service_schema({"some": str})), + probatio.Schema(cv.make_entity_service_schema({"some": str})), + probatio.All(cv.make_entity_service_schema({"some": str})), ) ): service.async_register_platform_entity_service( diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index eb7e1ca8ec4fa3..6329b9fb81ee6d 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -10,9 +10,9 @@ from unittest.mock import ANY, AsyncMock, MagicMock, Mock, call, patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest from pytest_unordered import unordered -import voluptuous as vol from homeassistant.components import automation from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA @@ -187,7 +187,7 @@ async def _call_in_order(funcs: list[Callable[[], Any]], *, reverse: bool) -> li async def test_bad_trigger_platform(hass: HomeAssistant) -> None: """Test bad trigger platform.""" - with pytest.raises(vol.Invalid) as ex: + with pytest.raises(probatio.Invalid) as ex: await async_validate_trigger_config(hass, [{"platform": "not_a_platform"}]) assert "Invalid trigger 'not_a_platform' specified" in str(ex) @@ -913,7 +913,7 @@ async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]: assert await async_validate_trigger_config(hass, config_1) == config_1 assert await async_validate_trigger_config(hass, config_2) == config_2 with pytest.raises( - vol.Invalid, match="Invalid trigger 'test.unknown_trig' specified" + probatio.Invalid, match="Invalid trigger 'test.unknown_trig' specified" ): await async_validate_trigger_config(hass, config_3) @@ -962,8 +962,8 @@ async def test_platform_migrate_trigger(hass: HomeAssistant) -> None: """Test a trigger platform with a migration.""" OPTIONS_SCHEMA_DICT = { - vol.Required("option_1"): str, - vol.Optional("option_2"): int, + probatio.Required("option_1"): str, + probatio.Optional("option_2"): int, } class MockTrigger(Trigger): @@ -1015,8 +1015,8 @@ class MockTriggerPlatform: TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { - vol.Required("option_1"): str, - vol.Optional("option_2"): int, + probatio.Required("option_1"): str, + probatio.Optional("option_2"): int, } ) @@ -1529,12 +1529,12 @@ async def good_subscriber(new_triggers: set[str]): ( # Missing threshold type {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Invalid threshold type {"threshold": {"type": "invalid_type"}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must be valid entity id @@ -1545,7 +1545,7 @@ async def good_subscriber(new_triggers: set[str]): "value_max": {"entity": "dog"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Above must be smaller than below @@ -1556,7 +1556,7 @@ async def good_subscriber(new_triggers: set[str]): "value_max": {"number": 10}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) @@ -1685,21 +1685,21 @@ class _TestChangedTrigger( # Invalid: missing threshold type ( {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: invalid threshold type ( {"threshold": {"type": "invalid_type"}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: numerical limit without unit ( {"threshold": {"type": "above", "value": {"number": 10}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( {"threshold": {"type": "below", "value": {"number": 90}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( { @@ -1709,7 +1709,7 @@ class _TestChangedTrigger( "value_max": {"number": 90}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: one numerical limit without unit (other is entity) ( @@ -1720,7 +1720,7 @@ class _TestChangedTrigger( "value_max": {"entity": "sensor.test"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( { @@ -1730,7 +1730,7 @@ class _TestChangedTrigger( "value_max": {"number": 90}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: invalid unit value ( @@ -1740,7 +1740,7 @@ class _TestChangedTrigger( "value": {"number": 10, "unit_of_measurement": "invalid_unit"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: Must use valid entity id ( @@ -1751,7 +1751,7 @@ class _TestChangedTrigger( "value_max": {"entity": "dog"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: above must be smaller than below ( @@ -1762,7 +1762,7 @@ class _TestChangedTrigger( "value_max": {"number": 10, "unit_of_measurement": "°F"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) @@ -3020,77 +3020,77 @@ async def test_numerical_trigger_reports_single_reason_for_between( ( # Missing threshold type {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Missing threshold type {"threshold": {}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Invalid threshold type {"threshold": {"type": "cat"}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide lower limit for ABOVE {"threshold": {"type": "above"}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide lower limit for ABOVE {"threshold": {"type": "above", "value_min": {"number": 10}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide lower limit for ABOVE {"threshold": {"type": "above", "value_max": {"number": 90}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper limit for BELOW {"threshold": {"type": "below"}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper limit for BELOW {"threshold": {"type": "below", "value_min": {"number": 10}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper limit for BELOW {"threshold": {"type": "below", "value_max": {"number": 10}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper and lower limits for BETWEEN {"threshold": {"type": "between"}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper and lower limits for BETWEEN {"threshold": {"type": "between", "value_min": {"number": 10}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper and lower limits for BETWEEN {"threshold": {"type": "between", "value_max": {"number": 90}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper and lower limits for OUTSIDE {"threshold": {"type": "outside"}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper and lower limits for OUTSIDE {"threshold": {"type": "outside", "value_min": {"number": 10}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must provide upper and lower limits for OUTSIDE {"threshold": {"type": "outside", "value_max": {"number": 90}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Must be valid entity id @@ -3101,7 +3101,7 @@ async def test_numerical_trigger_reports_single_reason_for_between( "value_max": {"entity": "dog"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( # Min must be smaller than max @@ -3112,7 +3112,7 @@ async def test_numerical_trigger_reports_single_reason_for_between( "value_max": {"number": 10}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) @@ -3228,7 +3228,7 @@ class _TestCrossedThresholdTrigger( # Invalid: numerical limit without unit ( {"threshold": {"type": "above", "value": {"number": 10}}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ( { @@ -3238,7 +3238,7 @@ class _TestCrossedThresholdTrigger( "value_max": {"number": 90}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: one numerical limit without unit (other is entity) ( @@ -3249,7 +3249,7 @@ class _TestCrossedThresholdTrigger( "value_max": {"entity": "sensor.test"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: invalid unit value ( @@ -3259,17 +3259,17 @@ class _TestCrossedThresholdTrigger( "value": {"number": 10, "unit_of_measurement": "invalid_unit"}, } }, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: missing threshold type ( {}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), # Invalid: missing threshold type ( {"threshold": {}}, - pytest.raises(vol.Invalid), + pytest.raises(probatio.Invalid), ), ], ) @@ -6091,7 +6091,9 @@ async def mock_device_automation(hass: HomeAssistant) -> str: hass, "test.device_trigger", Mock( - TRIGGER_SCHEMA=DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + TRIGGER_SCHEMA=DEVICE_TRIGGER_BASE_SCHEMA.extend( + {}, extra=probatio.ALLOW_EXTRA + ) ), ) config_entry = MockConfigEntry(domain="test") @@ -6273,5 +6275,5 @@ def test_entity_state_trigger_schema_behavior_invalid(behavior: str) -> None: CONF_TARGET: {CONF_ENTITY_ID: "test.entity"}, CONF_OPTIONS: {ATTR_BEHAVIOR: behavior}, } - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): ENTITY_STATE_TRIGGER_SCHEMA_WITH_BEHAVIOR(config) diff --git a/tests/pylint/actions/test_swallowed_exceptions.py b/tests/pylint/actions/test_swallowed_exceptions.py index e8048901c07cfa..8b85f9ac1ee87b 100644 --- a/tests/pylint/actions/test_swallowed_exceptions.py +++ b/tests/pylint/actions/test_swallowed_exceptions.py @@ -460,7 +460,7 @@ async def async_setup_entry(hass, entry, async_add_entities): platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( "set_speed", - {vol.Required("speed"): cv.string}, + {probatio.Required("speed"): cv.string}, "async_set_speed", ) @@ -491,7 +491,7 @@ async def async_setup_entry(hass, entry, async_add_entities): platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( "set_speed", - {vol.Required("speed"): cv.string}, + {probatio.Required("speed"): cv.string}, "async_set_speed", ) diff --git a/tests/pylint/config_flow/test_no_name.py b/tests/pylint/config_flow/test_no_name.py index 5753aaf535cd20..91ade175d8a66e 100644 --- a/tests/pylint/config_flow/test_no_name.py +++ b/tests/pylint/config_flow/test_no_name.py @@ -18,35 +18,35 @@ [ pytest.param( """ - vol.Required(CONF_HOST) + probatio.Required(CONF_HOST) """, "homeassistant.components.test.config_flow", id="non_name_field", ), pytest.param( """ - vol.Optional("password") + probatio.Optional("password") """, "homeassistant.components.test.config_flow", id="non_name_string_field", ), pytest.param( """ - vol.Optional("name") + probatio.Optional("name") """, "homeassistant.components.test.sensor", id="name_in_sensor_not_flagged", ), pytest.param( """ - vol.Optional(CONF_NAME) + probatio.Optional(CONF_NAME) """, "some.other.module", id="outside_components", ), pytest.param( """ - vol.Optional("name") + probatio.Optional("name") """, "homeassistant.components.test", id="name_in_init_not_flagged", @@ -71,28 +71,28 @@ def test_enforce_config_flow_no_name( [ pytest.param( """ - vol.Required(CONF_NAME) + probatio.Required(CONF_NAME) """, "homeassistant.components.test.config_flow", id="conf_name", ), pytest.param( """ - vol.Optional("name", default="My Device") + probatio.Optional("name", default="My Device") """, "homeassistant.components.test.config_flow", id="string_name", ), pytest.param( """ - vol.Required("device_name") + probatio.Required("device_name") """, "homeassistant.components.test.config_flow", id="device_name", ), pytest.param( """ - vol.Optional(CONF_DEVICE_NAME) + probatio.Optional(CONF_DEVICE_NAME) """, "homeassistant.components.test.config_flow", id="conf_device_name", @@ -123,8 +123,8 @@ def test_enforce_config_flow_no_name_subentry_flow( class MySubentryFlowHandler(ConfigSubentryFlow): async def async_step_user(self, user_input=None): return self.async_show_form( - data_schema=vol.Schema({ - vol.Required(CONF_NAME): str, + data_schema=probatio.Schema({ + probatio.Required(CONF_NAME): str, }) ) """ @@ -148,7 +148,7 @@ def test_enforce_config_flow_no_name_helper_integration( ) code = """ - vol.Required(CONF_NAME) + probatio.Required(CONF_NAME) """ root_node = astroid.parse(code, "homeassistant.components.my_helper.config_flow") root_node.file = str(integration_dir / "config_flow.py") @@ -169,7 +169,7 @@ def test_enforce_config_flow_no_name_non_helper_integration( (integration_dir / "manifest.json").write_text(json.dumps({"domain": "my_device"})) code = """ - vol.Required(CONF_NAME) + probatio.Required(CONF_NAME) """ root_node = astroid.parse(code, "homeassistant.components.my_device.config_flow") root_node.file = str(integration_dir / "config_flow.py") diff --git a/tests/pylint/config_flow/test_no_polling.py b/tests/pylint/config_flow/test_no_polling.py index 3e0084cccd62c5..497d142e4f38cd 100644 --- a/tests/pylint/config_flow/test_no_polling.py +++ b/tests/pylint/config_flow/test_no_polling.py @@ -13,49 +13,49 @@ [ pytest.param( """ - vol.Required(CONF_HOST) + probatio.Required(CONF_HOST) """, "homeassistant.components.test.config_flow", id="non_polling_field", ), pytest.param( """ - vol.Optional("username") + probatio.Optional("username") """, "homeassistant.components.test.config_flow", id="non_polling_string_field", ), pytest.param( """ - vol.Optional(CONF_SCAN_INTERVAL) + probatio.Optional(CONF_SCAN_INTERVAL) """, "homeassistant.components.test.sensor", id="polling_in_sensor_not_flagged", ), pytest.param( """ - vol.Optional(CONF_SCAN_INTERVAL) + probatio.Optional(CONF_SCAN_INTERVAL) """, "some.other.module", id="outside_components", ), pytest.param( """ - vol.Optional("scan_interval", default=30) + probatio.Optional("scan_interval", default=30) """, "homeassistant.components.test", id="polling_in_init_not_flagged", ), pytest.param( """ - vol.Optional("check_interval") + probatio.Optional("check_interval") """, "homeassistant.components.test.config_flow", id="unknown_interval_field", ), pytest.param( """ - vol.Optional("poll_frequency") + probatio.Optional("poll_frequency") """, "homeassistant.components.test.config_flow", id="unknown_frequency_field", @@ -80,42 +80,42 @@ def test_enforce_config_flow_no_polling( [ pytest.param( """ - vol.Optional(CONF_SCAN_INTERVAL) + probatio.Optional(CONF_SCAN_INTERVAL) """, "homeassistant.components.test.config_flow", id="conf_scan_interval", ), pytest.param( """ - vol.Optional("scan_interval", default=30) + probatio.Optional("scan_interval", default=30) """, "homeassistant.components.test.config_flow", id="string_scan_interval", ), pytest.param( """ - vol.Required("update_interval") + probatio.Required("update_interval") """, "homeassistant.components.test.config_flow", id="update_interval", ), pytest.param( """ - vol.Optional("update_frequency", default=60) + probatio.Optional("update_frequency", default=60) """, "homeassistant.components.test.config_flow", id="update_frequency", ), pytest.param( """ - vol.Optional("refresh_interval") + probatio.Optional("refresh_interval") """, "homeassistant.components.test.config_flow", id="refresh_interval", ), pytest.param( """ - vol.Optional(CONF_UPDATE_INTERVAL) + probatio.Optional(CONF_UPDATE_INTERVAL) """, "homeassistant.components.test.config_flow", id="conf_update_interval", diff --git a/tests/pylint/config_flow/test_serial_port_usb_dependency.py b/tests/pylint/config_flow/test_serial_port_usb_dependency.py index 8d637cca07b70c..fb105eff9e2fe4 100644 --- a/tests/pylint/config_flow/test_serial_port_usb_dependency.py +++ b/tests/pylint/config_flow/test_serial_port_usb_dependency.py @@ -33,7 +33,7 @@ def test_serial_port_selector_with_usb_dependency( ) code = """ - vol.Schema({vol.Required(CONF_PORT): SerialPortSelector()}) + probatio.Schema({probatio.Required(CONF_PORT): SerialPortSelector()}) """ root_node = astroid.parse(code, "homeassistant.components.my_device.config_flow") root_node.file = str(integration_dir / "config_flow.py") @@ -47,7 +47,7 @@ def test_serial_port_selector_with_usb_dependency( [ pytest.param( """ - vol.Schema({vol.Required(CONF_HOST): TextSelector()}) + probatio.Schema({probatio.Required(CONF_HOST): TextSelector()}) """, "homeassistant.components.my_device.config_flow", id="other_selector", @@ -98,9 +98,9 @@ def test_serial_port_selector_without_usb_dependency( integration_dir = _write_integration(tmp_path, "my_device", manifest) code = """ - vol.Schema({ - vol.Required(CONF_PORT): SerialPortSelector(), - vol.Optional(CONF_OTHER): SerialPortSelector(), + probatio.Schema({ + probatio.Required(CONF_PORT): SerialPortSelector(), + probatio.Optional(CONF_OTHER): SerialPortSelector(), }) """ root_node = astroid.parse(code, "homeassistant.components.my_device.config_flow") diff --git a/tests/pylint/test_flow_translations.py b/tests/pylint/test_flow_translations.py index baed11cbd0643c..44e1ad8ebc0b4a 100644 --- a/tests/pylint/test_flow_translations.py +++ b/tests/pylint/test_flow_translations.py @@ -53,9 +53,9 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("host"): str, - vol.Optional("port"): int, + data_schema=probatio.Schema({ + probatio.Required("host"): str, + probatio.Optional("port"): int, }), ) """, @@ -87,9 +87,9 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("host"): str, - vol.Required("missing_field"): str, + data_schema=probatio.Schema({ + probatio.Required("host"): str, + probatio.Required("missing_field"): str, }), ) """, @@ -127,8 +127,8 @@ class MyOptionsFlow(OptionsFlow): async def async_step_init(self, user_input=None): return self.async_show_form( step_id="init", - data_schema=vol.Schema({ - vol.Required("interval"): int, + data_schema=probatio.Schema({ + probatio.Required("interval"): int, }), ) """, @@ -160,9 +160,9 @@ class MyOptionsFlow(OptionsFlow): async def async_step_init(self, user_input=None): return self.async_show_form( step_id="init", - data_schema=vol.Schema({ - vol.Required("interval"): int, - vol.Required("missing"): str, + data_schema=probatio.Schema({ + probatio.Required("interval"): int, + probatio.Required("missing"): str, }), ) """, @@ -211,8 +211,8 @@ class MySubentryFlow(ConfigSubentryFlow): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("name"): str, + data_schema=probatio.Schema({ + probatio.Required("name"): str, }), ) """, @@ -255,9 +255,9 @@ class MySubentryFlow(ConfigSubentryFlow): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("name"): str, - vol.Required("missing"): str, + data_schema=probatio.Schema({ + probatio.Required("name"): str, + probatio.Required("missing"): str, }), ) """, @@ -306,8 +306,8 @@ class SharedFlow(ConfigSubentryFlow): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("name"): str, + data_schema=probatio.Schema({ + probatio.Required("name"): str, }), ) """, @@ -357,11 +357,11 @@ class MySubentryFlow(ConfigSubentryFlow): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("advanced"): section( - vol.Schema({ - vol.Required("timeout"): int, - vol.Required("retries"): int, + data_schema=probatio.Schema({ + probatio.Required("advanced"): section( + probatio.Schema({ + probatio.Required("timeout"): int, + probatio.Required("retries"): int, }), ), }), @@ -397,13 +397,13 @@ def test_schema_extend_base_field_flagged( root_node = astroid.parse( """ -BASE_SCHEMA = vol.Schema({vol.Required("host"): str}) +BASE_SCHEMA = probatio.Schema({probatio.Required("host"): str}) class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=BASE_SCHEMA.extend({vol.Optional("port"): int}), + data_schema=BASE_SCHEMA.extend({probatio.Optional("port"): int}), ) """, "homeassistant.components.test_int.config_flow", @@ -433,13 +433,13 @@ def test_schema_extend_all_translated_ok( root_node = astroid.parse( """ -BASE_SCHEMA = vol.Schema({vol.Required("host"): str}) +BASE_SCHEMA = probatio.Schema({probatio.Required("host"): str}) class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=BASE_SCHEMA.extend({vol.Optional("port"): int}), + data_schema=BASE_SCHEMA.extend({probatio.Optional("port"): int}), ) """, "homeassistant.components.test_int.config_flow", @@ -466,14 +466,14 @@ def test_schema_extend_via_schema_attribute_flagged( root_node = astroid.parse( """ -BASE_SCHEMA = vol.Schema({vol.Required("missing"): str}) +BASE_SCHEMA = probatio.Schema({probatio.Required("missing"): str}) class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema( - {vol.Required("host"): str} + data_schema=probatio.Schema( + {probatio.Required("host"): str} ).extend(BASE_SCHEMA.schema), ) """, @@ -504,9 +504,9 @@ def test_add_suggested_values_to_schema_missing_field_flagged( root_node = astroid.parse( """ -OPTIONS_SCHEMA = vol.Schema({ - vol.Required("interval"): int, - vol.Required("missing"): str, +OPTIONS_SCHEMA = probatio.Schema({ + probatio.Required("interval"): int, + probatio.Required("missing"): str, }) class MyOptionsFlow(OptionsFlow): @@ -545,9 +545,9 @@ def test_add_suggested_values_to_schema_keyword_flagged( root_node = astroid.parse( """ -USER_SCHEMA = vol.Schema({ - vol.Required("host"): str, - vol.Required("missing"): str, +USER_SCHEMA = probatio.Schema({ + probatio.Required("host"): str, + probatio.Required("missing"): str, }) class MyConfigFlow(ConfigFlow, domain=DOMAIN): @@ -586,7 +586,7 @@ def test_add_suggested_values_to_schema_translated_ok( root_node = astroid.parse( """ -OPTIONS_SCHEMA = vol.Schema({vol.Required("interval"): int}) +OPTIONS_SCHEMA = probatio.Schema({probatio.Required("interval"): int}) class MyOptionsFlow(OptionsFlow): async def async_step_init(self, user_input=None): @@ -627,7 +627,7 @@ def test_implicit_step_id_from_method( class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( - data_schema=vol.Schema({vol.Required("host"): str}), + data_schema=probatio.Schema({probatio.Required("host"): str}), ) """, "homeassistant.components.test_int.config_flow", @@ -657,9 +657,9 @@ def test_implicit_step_id_missing_field_flagged( class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( - data_schema=vol.Schema({ - vol.Required("host"): str, - vol.Required("missing"): str, + data_schema=probatio.Schema({ + probatio.Required("host"): str, + probatio.Required("missing"): str, }), ) """, @@ -698,7 +698,7 @@ async def async_step_user(self, user_input=None): step = "other" return self.async_show_form( step_id=step, - data_schema=vol.Schema({vol.Required("missing"): str}), + data_schema=probatio.Schema({probatio.Required("missing"): str}), ) """, "homeassistant.components.test_int.config_flow", @@ -742,12 +742,12 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("host"): str, - vol.Required("advanced"): section( - vol.Schema({ - vol.Required("ssl"): bool, - vol.Required("verify"): bool, + data_schema=probatio.Schema({ + probatio.Required("host"): str, + probatio.Required("advanced"): section( + probatio.Schema({ + probatio.Required("ssl"): bool, + probatio.Required("verify"): bool, }), {"collapsed": True}, ), @@ -795,12 +795,12 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("host"): str, - vol.Required("advanced"): section( - vol.Schema({ - vol.Required("ssl"): bool, - vol.Required("missing_field"): bool, + data_schema=probatio.Schema({ + probatio.Required("host"): str, + probatio.Required("advanced"): section( + probatio.Schema({ + probatio.Required("ssl"): bool, + probatio.Required("missing_field"): bool, }), {"collapsed": True}, ), @@ -850,10 +850,10 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Required("host"): str, - vol.Required("advanced"): data_entry_flow.section( - vol.Schema({vol.Required("ssl"): bool}), + data_schema=probatio.Schema({ + probatio.Required("host"): str, + probatio.Required("advanced"): data_entry_flow.section( + probatio.Schema({probatio.Required("ssl"): bool}), {"collapsed": True}, ), }), @@ -875,7 +875,7 @@ def test_inclusive_exclusive_markers_resolved( flow_translations_checker: ConfigFlowTranslationsChecker, tmp_path: Path, ) -> None: - """Fields marked vol.Inclusive/vol.Exclusive are checked.""" + """Fields marked probatio.Inclusive/probatio.Exclusive are checked.""" integration_dir = _make_integration( tmp_path, {"config": {"step": {"user": {"data": {"password": "Password"}}}}}, @@ -887,9 +887,9 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Inclusive("password", "encrypted"): str, - vol.Exclusive("missing", "auth"): str, + data_schema=probatio.Schema({ + probatio.Inclusive("password", "encrypted"): str, + probatio.Exclusive("missing", "auth"): str, }), ) """, @@ -912,7 +912,7 @@ def test_bare_marker_names_resolved( flow_translations_checker: ConfigFlowTranslationsChecker, tmp_path: Path, ) -> None: - """Markers imported directly from voluptuous are checked.""" + """Markers imported directly from probatio are checked.""" integration_dir = _make_integration( tmp_path, {"config": {"step": {"user": {"data": {"host": "Host"}}}}}, @@ -924,7 +924,7 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({ + data_schema=probatio.Schema({ Required("host"): str, Optional("missing"): str, }), @@ -957,9 +957,9 @@ def test_annotated_schema_variable_resolved( root_node = astroid.parse( """ -CONFIG_SCHEMA: Final = vol.Schema({ - vol.Required("host"): str, - vol.Required("missing"): str, +CONFIG_SCHEMA: Final = probatio.Schema({ + probatio.Required("host"): str, + probatio.Required("missing"): str, }) class MyConfigFlow(ConfigFlow, domain=DOMAIN): @@ -999,7 +999,7 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + data_schema=probatio.Schema({probatio.Required(CONF_HOST): str}), ) """, "homeassistant.components.test_int.config_flow", @@ -1026,13 +1026,13 @@ def test_dict_unpacking_in_schema( root_node = astroid.parse( """ -BASE = {vol.Required("host"): str} +BASE = {probatio.Required("host"): str} class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({**BASE, vol.Optional("port"): int}), + data_schema=probatio.Schema({**BASE, probatio.Optional("port"): int}), ) """, "homeassistant.components.test_int.config_flow", @@ -1059,7 +1059,7 @@ def test_schema_variable_resolved( root_node = astroid.parse( """ -USER_SCHEMA = vol.Schema({vol.Required("host"): str}) +USER_SCHEMA = probatio.Schema({probatio.Required("host"): str}) class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): @@ -1093,7 +1093,7 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("host"): str}), + data_schema=probatio.Schema({probatio.Required("host"): str}), ) """, "homeassistant.components.test_int.config_flow", @@ -1123,7 +1123,7 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("host"): str}), + data_schema=probatio.Schema({probatio.Required("host"): str}), ) """, "homeassistant.components.test_int.sensor", @@ -1153,7 +1153,7 @@ class MyFlowMixin(SomeUnresolvableBase): async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required("host"): str}), + data_schema=probatio.Schema({probatio.Required("host"): str}), ) """, "homeassistant.components.test_int.config_flow", @@ -1187,8 +1187,8 @@ class MyOptionsFlowHandler(BaseMixinFlowHandler, OptionsFlow): async def async_step_init(self, user_input=None): return self.async_show_form( step_id="init", - data_schema=vol.Schema({ - vol.Required("interval"): int, + data_schema=probatio.Schema({ + probatio.Required("interval"): int, }), ) """, diff --git a/tests/syrupy.py b/tests/syrupy.py index 2fe63535110536..f35c8ada47b4c0 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -8,11 +8,10 @@ import attr import attrs -from probatio import to_field_list +import probatio from syrupy.extensions.amber import AmberDataSerializer, AmberSnapshotExtension from syrupy.location import PyTestLocation from syrupy.types import PropertyFilter, PropertyMatcher, PropertyPath, SerializableData -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import State @@ -118,8 +117,8 @@ def _serialize( "continue_conversation", }: serializable_data = cls._serializable_conversation_result(data) - elif isinstance(data, vol.Schema): - serializable_data = to_field_list(data) + elif isinstance(data, probatio.Schema): + serializable_data = probatio.to_field_list(data) elif isinstance(data, ConfigEntry): serializable_data = cls._serializable_config_entry(data) elif dataclasses.is_dataclass(type(data)): diff --git a/tests/test_config.py b/tests/test_config.py index c7bcf59902fbd3..5bf09eb309a39d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,9 +10,9 @@ from unittest import mock from unittest.mock import AsyncMock, MagicMock, Mock, patch +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol import yaml from homeassistant import config as config_util, loader @@ -76,14 +76,16 @@ def teardown(): os.remove(SAFE_MODE_PATH) -IOT_DOMAIN_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({vol.Remove("old"): str}) +IOT_DOMAIN_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({probatio.Remove("old"): str}) @pytest.fixture async def mock_iot_domain_integration(hass: HomeAssistant) -> Integration: """Mock an integration which provides an IoT domain.""" - comp_platform_schema = cv.PLATFORM_SCHEMA.extend({vol.Remove("old"): str}) - comp_platform_schema_base = comp_platform_schema.extend({}, extra=vol.ALLOW_EXTRA) + comp_platform_schema = cv.PLATFORM_SCHEMA.extend({probatio.Remove("old"): str}) + comp_platform_schema_base = comp_platform_schema.extend( + {}, extra=probatio.ALLOW_EXTRA + ) return mock_integration( hass, @@ -98,8 +100,10 @@ async def mock_iot_domain_integration(hass: HomeAssistant) -> Integration: @pytest.fixture async def mock_iot_domain_integration_with_docs(hass: HomeAssistant) -> Integration: """Mock an integration which provides an IoT domain.""" - comp_platform_schema = cv.PLATFORM_SCHEMA.extend({vol.Remove("old"): str}) - comp_platform_schema_base = comp_platform_schema.extend({}, extra=vol.ALLOW_EXTRA) + comp_platform_schema = cv.PLATFORM_SCHEMA.extend({probatio.Remove("old"): str}) + comp_platform_schema_base = comp_platform_schema.extend( + {}, extra=probatio.ALLOW_EXTRA + ) return mock_integration( hass, @@ -123,7 +127,7 @@ async def mock_non_adr_0007_integration(hass: HomeAssistant) -> None: """ test_platform_schema = IOT_DOMAIN_PLATFORM_SCHEMA.extend( - {vol.Required("option1"): str, vol.Optional("option2"): str} + {probatio.Required("option1"): str, probatio.Optional("option2"): str} ) mock_platform( hass, @@ -150,7 +154,7 @@ async def mock_non_adr_0007_integration_with_docs(hass: HomeAssistant) -> None: ), ) test_platform_schema = IOT_DOMAIN_PLATFORM_SCHEMA.extend( - {vol.Required("option1"): str, vol.Optional("option2"): str} + {probatio.Required("option1"): str, probatio.Optional("option2"): str} ) mock_platform( hass, @@ -170,16 +174,16 @@ async def mock_adr_0007_integrations(hass: HomeAssistant) -> list[Integration]: "adr_0007_4", "adr_0007_5", ): - adr_0007_config_schema = vol.Schema( + adr_0007_config_schema = probatio.Schema( { - domain: vol.Schema( + domain: probatio.Schema( { - vol.Required("host"): str, - vol.Optional("port", default=8080): int, + probatio.Required("host"): str, + probatio.Optional("port", default=8080): int, } ) }, - extra=vol.ALLOW_EXTRA, + extra=probatio.ALLOW_EXTRA, ) integrations.append( mock_integration( @@ -203,16 +207,16 @@ async def mock_adr_0007_integrations_with_docs( "adr_0007_4", "adr_0007_5", ): - adr_0007_config_schema = vol.Schema( + adr_0007_config_schema = probatio.Schema( { - domain: vol.Schema( + domain: probatio.Schema( { - vol.Required("host"): str, - vol.Optional("port", default=8080): int, + probatio.Required("host"): str, + probatio.Optional("port", default=8080): int, } ) }, - extra=vol.ALLOW_EXTRA, + extra=probatio.ALLOW_EXTRA, ) integrations.append( mock_integration( @@ -237,16 +241,16 @@ async def mock_custom_validator_integrations(hass: HomeAssistant) -> list[Integr for domain in ("custom_validator_ok_1", "custom_validator_ok_2"): def gen_async_validate_config(domain): - schema = vol.Schema( + schema = probatio.Schema( { - domain: vol.Schema( + domain: probatio.Schema( { - vol.Required("host"): str, - vol.Optional("port", default=8080): int, + probatio.Required("host"): str, + probatio.Optional("port", default=8080): int, } ) }, - extra=vol.ALLOW_EXTRA, + extra=probatio.ALLOW_EXTRA, ) async def async_validate_config( @@ -286,16 +290,16 @@ async def mock_custom_validator_integrations_with_docs( for domain in ("custom_validator_ok_1", "custom_validator_ok_2"): def gen_async_validate_config(domain): - schema = vol.Schema( + schema = probatio.Schema( { - domain: vol.Schema( + domain: probatio.Schema( { - vol.Required("host"): str, - vol.Optional("port", default=8080): int, + probatio.Required("host"): str, + probatio.Optional("port", default=8080): int, } ) }, - extra=vol.ALLOW_EXTRA, + extra=probatio.ALLOW_EXTRA, ) async def async_validate_config( @@ -1219,7 +1223,7 @@ async def test_component_config_exceptions( ( [ config_util.ConfigExceptionInfo( - vol.Invalid("bla", ["path"]), + probatio.Invalid("bla", ["path"]), "config_validation_err", "test_domain", ConfigTestClass({"test_domain": []}), @@ -1239,7 +1243,7 @@ async def test_component_config_exceptions( ( [ config_util.ConfigExceptionInfo( - vol.Invalid("bla", ["path"]), + probatio.Invalid("bla", ["path"]), "platform_config_validation_err", "test_domain", ConfigTestClass({"test_domain": []}), @@ -1327,30 +1331,44 @@ async def test_component_config_error_processing( @pytest.mark.parametrize( ("domain", "schema", "expected"), [ - ("zone", vol.Schema({vol.Optional("zone", default=list): [int]}), "list"), - ("zone", vol.Schema({vol.Optional("zone", default=[]): [int]}), "list"), ( "zone", - vol.Schema({vol.Optional("zone", default={}): {vol.Optional("hello"): 1}}), + probatio.Schema({probatio.Optional("zone", default=list): [int]}), + "list", + ), + ( + "zone", + probatio.Schema({probatio.Optional("zone", default=[]): [int]}), + "list", + ), + ( + "zone", + probatio.Schema( + {probatio.Optional("zone", default={}): {probatio.Optional("hello"): 1}} + ), "dict", ), ( "zone", - vol.Schema( - {vol.Optional("zone", default=dict): {vol.Optional("hello"): 1}} + probatio.Schema( + { + probatio.Optional("zone", default=dict): { + probatio.Optional("hello"): 1 + } + } ), "dict", ), - ("zone", vol.Schema({vol.Optional("zone"): int}), None), - ("zone", vol.Schema({"zone": int}), None), + ("zone", probatio.Schema({probatio.Optional("zone"): int}), None), + ("zone", probatio.Schema({"zone": int}), None), ( "not_existing", - vol.Schema({vol.Optional("zone", default=dict): dict}), + probatio.Schema({probatio.Optional("zone", default=dict): dict}), None, ), - ("non_existing", vol.Schema({"zone": int}), None), - ("zone", vol.Schema({}), None), - ("plex", vol.Schema(vol.All({"plex": {"host": str}})), "dict"), + ("non_existing", probatio.Schema({"zone": int}), None), + ("zone", probatio.Schema({}), None), + ("plex", probatio.Schema(probatio.All({"plex": {"host": str}})), "dict"), ("openuv", cv.deprecated("openuv"), None), ], ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d7f13c2aef5d6c..01e69615044d3c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -11,9 +11,9 @@ from unittest.mock import ANY, AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant import config_entries, data_entry_flow, loader from homeassistant.config_entries import ConfigEntry @@ -10315,7 +10315,7 @@ async def async_step_init(self, user_input=None): if user_input is not None: return self.async_create_entry(data=user_input) return self.async_show_form( - step_id="init", data_schema=vol.Schema({"test": str}) + step_id="init", data_schema=probatio.Schema({"test": str}) ) return _OptionsFlow() diff --git a/tests/test_core.py b/tests/test_core.py index c393b5f5020924..697d655acb14a2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,9 +14,9 @@ from unittest.mock import MagicMock, patch from freezegun import freeze_time +import probatio import pytest from pytest_unordered import unordered -import voluptuous as vol from homeassistant import core as ha from homeassistant.const import ( @@ -2205,7 +2205,7 @@ async def test_service_call_event_contains_original_data(hass: HomeAssistant) -> events = async_capture_events(hass, EVENT_CALL_SERVICE) calls = async_mock_service( - hass, "test", "service", vol.Schema({"number": vol.Coerce(int)}) + hass, "test", "service", probatio.Schema({"number": probatio.Coerce(int)}) ) context = ha.Context() diff --git a/tests/test_core_config.py b/tests/test_core_config.py index e58874c6485e31..ad9128c5df2b83 100644 --- a/tests/test_core_config.py +++ b/tests/test_core_config.py @@ -9,8 +9,8 @@ from typing import Any from unittest.mock import Mock, PropertyMock, patch +from probatio import Invalid, MultipleInvalid import pytest -from voluptuous import Invalid, MultipleInvalid from webrtc_models import RTCConfiguration, RTCIceServer from homeassistant.const import ( diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index dcedc52aa9e25a..bbb3ec49c2ed87 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -5,8 +5,8 @@ import logging from unittest.mock import Mock, patch +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.core import Event, HomeAssistant, callback @@ -89,20 +89,24 @@ class TestFlow(data_entry_flow.FlowHandler): async def async_step_first(self, user_input=None): if user_input is not None: return await self.async_step_second() - return self.async_show_form(step_id="first", data_schema=vol.Schema([str])) + return self.async_show_form( + step_id="first", data_schema=probatio.Schema([str]) + ) async def async_step_second(self, user_input=None): if user_input is not None: return self.async_create_entry( title="Test Entry", data=self.init_data + user_input ) - return self.async_show_form(step_id="second", data_schema=vol.Schema([str])) + return self.async_show_form( + step_id="second", data_schema=probatio.Schema([str]) + ) form = await manager.async_init( "test", context={"init_step": "first"}, data=["INIT-DATA"] ) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): form = await manager.async_configure(form["flow_id"], "INCORRECT-DATA") form = await manager.async_configure(form["flow_id"], ["SECOND-DATA"]) @@ -116,7 +120,9 @@ async def async_step_second(self, user_input=None): async def test_show_form(manager: MockFlowManager) -> None: """Test that we can show a form.""" - schema = vol.Schema({vol.Required("username"): str, vol.Required("password"): str}) + schema = probatio.Schema( + {probatio.Required("username"): str, probatio.Required("password"): str} + ) @manager.mock_reg_handler("test") class TestFlow(data_entry_flow.FlowHandler): @@ -136,7 +142,9 @@ async def async_step_init(self, user_input=None): async def test_form_shows_with_added_suggested_values(manager: MockFlowManager) -> None: """Test that we can show a form with suggested values.""" - def compare_schemas(schema: vol.Schema, expected_schema: vol.Schema) -> None: + def compare_schemas( + schema: probatio.Schema, expected_schema: probatio.Schema + ) -> None: """Compare two schemas.""" assert schema.schema is not expected_schema.schema @@ -148,14 +156,14 @@ def compare_schemas(schema: vol.Schema, expected_schema: vol.Schema) -> None: continue assert validator == expected_schema.schema[key] - schema = vol.Schema( + schema = probatio.Schema( { - vol.Required("username"): str, - vol.Required("password"): str, - vol.Required("section_1"): data_entry_flow.section( - vol.Schema( + probatio.Required("username"): str, + probatio.Required("password"): str, + probatio.Required("section_1"): data_entry_flow.section( + probatio.Schema( { - vol.Optional("full_name"): str, + probatio.Optional("full_name"): str, } ), {"collapsed": False}, @@ -444,7 +452,7 @@ async def async_step_init(self, input): if input is not None: return self.async_create_entry(title="init", data=input) return self.async_show_form( - step_id="init", data_schema=vol.Schema({"count": int}) + step_id="init", data_schema=probatio.Schema({"count": int}) ) class FlowManager(data_entry_flow.FlowManager): @@ -457,7 +465,7 @@ async def async_finish_flow(self, flow, result): if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: if result["data"] is None or result["data"].get("count", 0) <= 1: return flow.async_show_form( - step_id="init", data_schema=vol.Schema({"count": int}) + step_id="init", data_schema=probatio.Schema({"count": int}) ) result["result"] = result["data"]["count"] return result @@ -1224,7 +1232,9 @@ class TestFlow(data_entry_flow.FlowHandler): async def async_step_first(self, user_input=None): if user_input is not None: return await self.async_step_second() - return self.async_show_form(step_id="first", data_schema=vol.Schema([str])) + return self.async_show_form( + step_id="first", data_schema=probatio.Schema([str]) + ) async def async_step_second(self, user_input=None): if user_input is not None: @@ -1232,7 +1242,9 @@ async def async_step_second(self, user_input=None): title="Test Entry", data={"init": self.init_data, "user": user_input}, ) - return self.async_show_form(step_id="second", data_schema=vol.Schema([str])) + return self.async_show_form( + step_id="second", data_schema=probatio.Schema([str]) + ) bluetooth_data = BluetoothDiscoveryData("aa:bb:cc:dd:ee:ff") wifi_data = WiFiDiscoveryData("host") @@ -1303,10 +1315,10 @@ def test_section_in_serializer() -> None: """Test section with custom_serializer.""" assert cv.custom_serializer( data_entry_flow.section( - vol.Schema( + probatio.Schema( { - vol.Optional("option_1", default=False): bool, - vol.Required("option_2"): int, + probatio.Optional("option_1", default=False): bool, + probatio.Required("option_2"): int, } ), {"collapsed": False}, @@ -1334,13 +1346,13 @@ def test_nested_section_in_serializer() -> None: ): cv.custom_serializer( data_entry_flow.section( - vol.Schema( + probatio.Schema( { - vol.Required("section_1"): data_entry_flow.section( - vol.Schema( + probatio.Required("section_1"): data_entry_flow.section( + probatio.Schema( { - vol.Optional("option_1", default=False): bool, - vol.Required("option_2"): int, + probatio.Optional("option_1", default=False): bool, + probatio.Required("option_2"): int, } ) ) diff --git a/tests/test_setup.py b/tests/test_setup.py index 7edd2f6c7d996f..6ec8edd37a68cf 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -5,8 +5,8 @@ from unittest.mock import ANY, AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory +import probatio import pytest -import voluptuous as vol from homeassistant import config_entries, loader, setup from homeassistant.config_entries import ConfigEntry @@ -51,7 +51,7 @@ class MockFlowHandler(config_entries.ConfigFlow): async def test_validate_component_config(hass: HomeAssistant) -> None: """Test validating component configuration.""" - config_schema = vol.Schema({"comp_conf": {"hello": str}}, required=True) + config_schema = probatio.Schema({"comp_conf": {"hello": str}}, required=True) mock_integration(hass, MockModule("comp_conf", config_schema=config_schema)) with assert_setup_component(0): @@ -584,7 +584,7 @@ def config_check_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: """Test platform that specifies config.""" platform_schema = cv.PLATFORM_SCHEMA.extend( - {"valid": True}, extra=vol.PREVENT_EXTRA + {"valid": True}, extra=probatio.PREVENT_EXTRA ) mock_setup = Mock(spec_set=True) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index ea5a95764eead4..b757bd1708c95e 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -2,9 +2,9 @@ import math +import probatio import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.util import color as color_util @@ -369,11 +369,11 @@ def test_color_below_6600_should_have_more_red_than_blue_or_green() -> None: assert rgb[0] > rgb[2] -def test_get_color_in_voluptuous() -> None: +def test_get_color_in_probatio() -> None: """Test using the get method in color validation.""" - schema = vol.Schema(color_util.color_name_to_rgb) + schema = probatio.Schema(color_util.color_name_to_rgb) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema("not a color") assert schema("red") == (255, 0, 0) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 94c3dd204f76b7..389c0ef962f990 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -8,8 +8,8 @@ from typing import Any from unittest.mock import Mock, patch +import probatio import pytest -import voluptuous as vol import yaml as pyyaml from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file @@ -529,20 +529,20 @@ def test_string_annotated() -> None: @pytest.mark.usefixtures("try_both_loaders") -def test_string_used_as_vol_schema() -> None: - """Test the subclassed strings can be used in voluptuous schemas.""" +def test_string_used_as_probatio_schema() -> None: + """Test the subclassed strings can be used in probatio schemas.""" conf = "wanted_data:\n key_1: value_1\n key_2: value_2\n" with io.StringIO(conf) as file: doc = yaml_loader.parse_yaml(file) # Test using the subclassed strings in a schema - schema = vol.Schema( - {vol.Required(key): value for key, value in doc["wanted_data"].items()}, + schema = probatio.Schema( + {probatio.Required(key): value for key, value in doc["wanted_data"].items()}, ) # Test using the subclassed strings when validating a schema schema(doc["wanted_data"]) schema({"key_1": "value_1", "key_2": "value_2"}) - with pytest.raises(vol.Invalid): + with pytest.raises(probatio.Invalid): schema({"key_1": "value_2", "key_2": "value_1"})