diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 3e19e03dc4b1a3..be98b19c09bfae 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -88,10 +88,6 @@ jobs: fail-fast: false matrix: arch: ${{ fromJson(needs.init.outputs.architectures) }} - exclude: - - arch: armv7 - - arch: armhf - - arch: i386 steps: - name: Checkout the repository uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e19d3203d977ae..41dde96d514c1c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -77,20 +77,8 @@ jobs: # Use C-Extension for SQLAlchemy echo "REQUIRE_SQLALCHEMY_CEXT=1" - - # Add additional pip wheel build constraints - echo "PIP_CONSTRAINT=build_constraints.txt" ) > .env_file - - name: Write pip wheel build constraints - run: | - ( - # ninja 1.11.1.2 + 1.11.1.3 seem to be broken on at least armhf - # this caused the numpy builds to fail - # https://github.com/scikit-build/ninja-python-distributions/issues/274 - echo "ninja==1.11.1.1" - ) > build_constraints.txt - - name: Upload env_file uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: @@ -99,13 +87,6 @@ jobs: include-hidden-files: true overwrite: true - - name: Upload build_constraints - uses: *actions-upload-artifact - with: - name: build_constraints - path: ./build_constraints.txt - overwrite: true - - name: Upload requirements_diff uses: *actions-upload-artifact with: @@ -138,13 +119,6 @@ jobs: - os: ubuntu-latest - arch: aarch64 os: ubuntu-24.04-arm - exclude: - - abi: cp314 - arch: armv7 - - abi: cp314 - arch: armhf - - abi: cp314 - arch: i386 steps: - *checkout @@ -154,12 +128,6 @@ jobs: with: name: env_file - - &download-build-constraints - name: Download build_constraints - uses: *actions-download-artifact - with: - name: build_constraints - - &download-requirements-diff name: Download requirements_diff uses: *actions-download-artifact @@ -199,7 +167,7 @@ jobs: - *checkout - *download-env-file - - *download-build-constraints + - *download-requirements-diff - name: Download requirements_all_wheels @@ -209,10 +177,6 @@ jobs: - name: Adjust build env run: | - if [ "${{ matrix.arch }}" = "i386" ]; then - echo "NPY_DISABLE_SVML=1" >> .env_file - fi - # Do not pin numpy in wheels building sed -i "/numpy/d" homeassistant/package_constraints.txt # Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine diff --git a/Dockerfile b/Dockerfile index aa4de12d3eb097..75153cef75e5b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,8 +21,6 @@ ARG BUILD_ARCH RUN \ case "${BUILD_ARCH}" in \ "aarch64") go2rtc_suffix='arm64' ;; \ - "armhf") go2rtc_suffix='armv6' ;; \ - "armv7") go2rtc_suffix='arm' ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \ esac \ && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.12/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ diff --git a/build.yaml b/build.yaml index 4994cdafcd7078..abf7b3cd7ce720 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,7 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.11.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.11.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.11.0 amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.11.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.11.0 cosign: base_identity: https://github.com/home-assistant/docker/.* identity: https://github.com/home-assistant/core/.* diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py deleted file mode 100644 index 6fccecfec5c924..00000000000000 --- a/homeassistant/components/dominos/__init__.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Support for Dominos Pizza ordering.""" - -from datetime import timedelta -import logging - -from pizzapi import Address, Customer, Order -import voluptuous as vol - -from homeassistant.components import http -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -# The domain of your component. Should be equal to the name of your component. -DOMAIN = "dominos" -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -ATTR_COUNTRY = "country_code" -ATTR_FIRST_NAME = "first_name" -ATTR_LAST_NAME = "last_name" -ATTR_EMAIL = "email" -ATTR_PHONE = "phone" -ATTR_ADDRESS = "address" -ATTR_ORDERS = "orders" -ATTR_SHOW_MENU = "show_menu" -ATTR_ORDER_ENTITY = "order_entity_id" -ATTR_ORDER_NAME = "name" -ATTR_ORDER_CODES = "codes" - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -MIN_TIME_BETWEEN_STORE_UPDATES = timedelta(minutes=3330) - -_ORDERS_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ORDER_NAME): cv.string, - vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]), - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(ATTR_COUNTRY): cv.string, - vol.Required(ATTR_FIRST_NAME): cv.string, - vol.Required(ATTR_LAST_NAME): cv.string, - vol.Required(ATTR_EMAIL): cv.string, - vol.Required(ATTR_PHONE): cv.string, - vol.Required(ATTR_ADDRESS): cv.string, - vol.Optional(ATTR_SHOW_MENU): cv.boolean, - vol.Optional(ATTR_ORDERS, default=[]): vol.All( - cv.ensure_list, [_ORDERS_SCHEMA] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up is called when Home Assistant is loading our component.""" - dominos = Dominos(hass, config) - - component = EntityComponent[DominosOrder](_LOGGER, DOMAIN, hass) - hass.data[DOMAIN] = {} - entities: list[DominosOrder] = [] - conf = config[DOMAIN] - - hass.services.register( - DOMAIN, - "order", - dominos.handle_order, - vol.Schema( - { - vol.Required(ATTR_ORDER_ENTITY): cv.entity_ids, - } - ), - ) - - if conf.get(ATTR_SHOW_MENU): - hass.http.register_view(DominosProductListView(dominos)) - - for order_info in conf.get(ATTR_ORDERS): - order = DominosOrder(order_info, dominos) - entities.append(order) - - component.add_entities(entities) - - # Return boolean to indicate that initialization was successfully. - return True - - -class Dominos: - """Main Dominos service.""" - - def __init__(self, hass, config): - """Set up main service.""" - conf = config[DOMAIN] - - self.hass = hass - self.customer = Customer( - conf.get(ATTR_FIRST_NAME), - conf.get(ATTR_LAST_NAME), - conf.get(ATTR_EMAIL), - conf.get(ATTR_PHONE), - conf.get(ATTR_ADDRESS), - ) - self.address = Address( - *self.customer.address.split(","), country=conf.get(ATTR_COUNTRY) - ) - self.country = conf.get(ATTR_COUNTRY) - try: - self.closest_store = self.address.closest_store() - except Exception: # noqa: BLE001 - self.closest_store = None - - def handle_order(self, call: ServiceCall) -> None: - """Handle ordering pizza.""" - entity_ids = call.data[ATTR_ORDER_ENTITY] - - target_orders = [ - order - for order in self.hass.data[DOMAIN]["entities"] - if order.entity_id in entity_ids - ] - - for order in target_orders: - order.place() - - @Throttle(MIN_TIME_BETWEEN_STORE_UPDATES) - def update_closest_store(self): - """Update the shared closest store (if open).""" - try: - self.closest_store = self.address.closest_store() - except Exception: # noqa: BLE001 - self.closest_store = None - return False - return True - - def get_menu(self): - """Return the products from the closest stores menu.""" - self.update_closest_store() - if self.closest_store is None: - _LOGGER.warning("Cannot get menu. Store may be closed") - return [] - menu = self.closest_store.get_menu() - product_entries = [] - - for product in menu.products: - item = {} - if isinstance(product.menu_data["Variants"], list): - variants = ", ".join(product.menu_data["Variants"]) - else: - variants = product.menu_data["Variants"] - item["name"] = product.name - item["variants"] = variants - product_entries.append(item) - - return product_entries - - -class DominosProductListView(http.HomeAssistantView): - """View to retrieve product list content.""" - - url = "/api/dominos" - name = "api:dominos" - - def __init__(self, dominos): - """Initialize suite view.""" - self.dominos = dominos - - @callback - def get(self, request): - """Retrieve if API is running.""" - return self.json(self.dominos.get_menu()) - - -class DominosOrder(Entity): - """Represents a Dominos order entity.""" - - def __init__(self, order_info, dominos): - """Set up the entity.""" - self._name = order_info["name"] - self._product_codes = order_info["codes"] - self._orderable = False - self.dominos = dominos - - @property - def name(self): - """Return the orders name.""" - return self._name - - @property - def product_codes(self): - """Return the orders product codes.""" - return self._product_codes - - @property - def orderable(self): - """Return the true if orderable.""" - return self._orderable - - @property - def state(self): - """Return the state either closed, orderable or unorderable.""" - if self.dominos.closest_store is None: - return "closed" - return "orderable" if self._orderable else "unorderable" - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update the order state and refreshes the store.""" - try: - self.dominos.update_closest_store() - except Exception: # noqa: BLE001 - self._orderable = False - return - - try: - order = self.order() - order.pay_with() - self._orderable = True - except Exception: # noqa: BLE001 - self._orderable = False - - def order(self): - """Create the order object.""" - if self.dominos.closest_store is None: - raise HomeAssistantError("No store available") - - order = Order( - self.dominos.closest_store, - self.dominos.customer, - self.dominos.address, - self.dominos.country, - ) - - for code in self._product_codes: - order.add_item(code) - - return order - - def place(self): - """Place the order.""" - try: - order = self.order() - order.place() - except Exception: # noqa: BLE001 - self._orderable = False - _LOGGER.warning( - "Attempted to order Dominos - Order invalid or store closed" - ) diff --git a/homeassistant/components/dominos/icons.json b/homeassistant/components/dominos/icons.json deleted file mode 100644 index ca33ac91dfd5e4..00000000000000 --- a/homeassistant/components/dominos/icons.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "services": { - "order": { - "service": "mdi:pizza" - } - } -} diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json deleted file mode 100644 index 5618c6f0d87685..00000000000000 --- a/homeassistant/components/dominos/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "dominos", - "name": "Dominos Pizza", - "codeowners": [], - "dependencies": ["http"], - "documentation": "https://www.home-assistant.io/integrations/dominos", - "iot_class": "cloud_polling", - "loggers": ["pizzapi"], - "quality_scale": "legacy", - "requirements": ["pizzapi==0.0.6"] -} diff --git a/homeassistant/components/dominos/services.yaml b/homeassistant/components/dominos/services.yaml deleted file mode 100644 index f2261072ddd182..00000000000000 --- a/homeassistant/components/dominos/services.yaml +++ /dev/null @@ -1,6 +0,0 @@ -order: - fields: - order_entity_id: - example: dominos.medium_pan - selector: - text: diff --git a/homeassistant/components/dominos/strings.json b/homeassistant/components/dominos/strings.json deleted file mode 100644 index eb6d5a8fc7364a..00000000000000 --- a/homeassistant/components/dominos/strings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "services": { - "order": { - "description": "Places a set of orders with Domino's Pizza.", - "fields": { - "order_entity_id": { - "description": "The ID (as specified in the configuration) of an order to place. If provided as an array, all the identified orders will be placed.", - "name": "Order entity" - } - }, - "name": "Order" - } - } -} diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 2e2c8133305880..ce7a8985df3a56 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -15,16 +15,20 @@ from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful from pylamarzocco.util import InstallationKey, generate_installation_key -from homeassistant.components.bluetooth import async_discovered_service_info +from homeassistant.components.bluetooth import ( + async_ble_device_from_address, + async_discovered_service_info, +) from homeassistant.const import ( CONF_MAC, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, Platform, __version__, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -99,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - # initialize Bluetooth bluetooth_client: LaMarzoccoBluetoothClient | None = None if entry.options.get(CONF_USE_BLUETOOTH, True) and ( - token := settings.ble_auth_token + token := (entry.data.get(CONF_TOKEN) or settings.ble_auth_token) ): if CONF_MAC not in entry.data: for discovery_info in async_discovered_service_info(hass): @@ -108,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - and name.startswith(BT_MODEL_PREFIXES) and name.split("_")[1] == serial ): - _LOGGER.debug("Found Bluetooth device, configuring with Bluetooth") + _LOGGER.info("Found lamarzocco Bluetooth device, adding to entry") # found a device, add MAC address to config entry hass.config_entries.async_update_entry( entry, @@ -118,22 +122,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - }, ) - if not entry.data[CONF_TOKEN]: - # update the token in the config entry - hass.config_entries.async_update_entry( - entry, - data={ - **entry.data, - CONF_TOKEN: token, - }, - ) - if CONF_MAC in entry.data: - _LOGGER.debug("Initializing Bluetooth device") - bluetooth_client = LaMarzoccoBluetoothClient( - address_or_ble_device=entry.data[CONF_MAC], - ble_token=token, - ) + ble_device = async_ble_device_from_address(hass, entry.data[CONF_MAC]) + if ble_device: + _LOGGER.info("Setting up lamarzocco with Bluetooth") + bluetooth_client = LaMarzoccoBluetoothClient( + ble_device=ble_device, + ble_token=token, + ) + + async def disconnect_bluetooth(_: Event) -> None: + """Stop push updates when hass stops.""" + await bluetooth_client.disconnect() + + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, disconnect_bluetooth + ) + ) + entry.async_on_unload(bluetooth_client.disconnect) + else: + _LOGGER.info( + "Bluetooth device not found during lamarzocco setup, continuing with cloud only" + ) device = LaMarzoccoMachine( serial_number=entry.unique_id, diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 126878fe72ee01..21fbd8ac9ba0cc 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.1.3"] + "requirements": ["pylamarzocco==2.2.0"] } diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 7266205549faef..6b8394200f0dec 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -129,6 +129,9 @@ class SmartThingsBinarySensorEntityDescription(BinarySensorEntityDescription): key=Attribute.REMOTE_CONTROL_ENABLED, translation_key="remote_control", is_on_key="true", + component_translation_key={ + "sub": "sub_remote_control", + }, ) }, Capability.SOUND_SENSOR: { diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index 2bdd562c8b9e4a..2f1ec0c982b38a 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -98,6 +98,7 @@ class SmartThingsSelectDescription(SelectEntityDescription): default_options: list[str] | None = None extra_components: list[str] | None = None capability_ignore_list: list[Capability] | None = None + value_is_integer: bool = False CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { @@ -185,6 +186,15 @@ class SmartThingsSelectDescription(SelectEntityDescription): options_map=WASHER_WATER_TEMPERATURE_TO_HA, entity_category=EntityCategory.CONFIG, ), + Capability.SAMSUNG_CE_DUST_FILTER_ALARM: SmartThingsSelectDescription( + key=Capability.SAMSUNG_CE_DUST_FILTER_ALARM, + translation_key="dust_filter_alarm", + options_attribute=Attribute.SUPPORTED_ALARM_THRESHOLDS, + status_attribute=Attribute.ALARM_THRESHOLD, + command=Command.SET_ALARM_THRESHOLD, + entity_category=EntityCategory.CONFIG, + value_is_integer=True, + ), } @@ -253,6 +263,8 @@ def options(self) -> list[str]: self.entity_description.options_map.get(option, option) for option in options ] + if self.entity_description.value_is_integer: + options = [str(option) for option in options] return options @property @@ -263,6 +275,8 @@ def current_option(self) -> str | None: ) if self.entity_description.options_map: option = self.entity_description.options_map.get(option) + if self.entity_description.value_is_integer and option is not None: + option = str(option) return option async def async_select_option(self, option: str) -> None: @@ -277,17 +291,20 @@ async def async_select_option(self, option: str) -> None: raise ServiceValidationError( "Can only be updated when remote control is enabled" ) + new_option: str | int = option if self.entity_description.options_map: - option = next( + new_option = next( ( key for key, value in self.entity_description.options_map.items() if value == option ), - option, + new_option, ) + if self.entity_description.value_is_integer: + new_option = int(option) await self.execute_device_command( self.entity_description.key, self.entity_description.command, - option, + new_option, ) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index e7f90c2b225a6c..e73caa67da263e 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -1054,6 +1054,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): translation_key="washer_machine_state", options=WASHER_OPTIONS, device_class=SensorDeviceClass.ENUM, + component_fn=lambda component: component == "sub", + component_translation_key={ + "sub": "washer_sub_machine_state", + }, ) ], Attribute.WASHER_JOB_STATE: [ @@ -1080,6 +1084,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): ], device_class=SensorDeviceClass.ENUM, value_fn=lambda value: JOB_STATE_MAP.get(value, value), + component_fn=lambda component: component == "sub", + component_translation_key={ + "sub": "washer_sub_job_state", + }, ) ], Attribute.COMPLETION_TIME: [ @@ -1088,6 +1096,10 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): translation_key="completion_time", device_class=SensorDeviceClass.TIMESTAMP, value_fn=dt_util.parse_datetime, + component_fn=lambda component: component == "sub", + component_translation_key={ + "sub": "washer_sub_completion_time", + }, ) ], }, diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index c8888ccfb42be6..a270b0c6cbcd85 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -66,6 +66,9 @@ "remote_control": { "name": "Remote control" }, + "sub_remote_control": { + "name": "Upper washer remote control" + }, "valve": { "name": "Valve" } @@ -161,6 +164,10 @@ "standard": "Standard" } }, + "dust_filter_alarm": { + "name": "Dust filter alarm threshold", + "unit_of_measurement": "hours" + }, "flexible_detergent_amount": { "name": "Flexible compartment dispense amount", "state": { @@ -636,6 +643,38 @@ "washer_mode": { "name": "Washer mode" }, + "washer_sub_completion_time": { + "name": "Upper washer completion time" + }, + "washer_sub_job_state": { + "name": "Upper washer job state", + "state": { + "ai_rinse": "[%key:component::smartthings::entity::sensor::washer_job_state::state::ai_rinse%]", + "ai_spin": "[%key:component::smartthings::entity::sensor::washer_job_state::state::ai_spin%]", + "ai_wash": "[%key:component::smartthings::entity::sensor::washer_job_state::state::ai_wash%]", + "air_wash": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::air_wash%]", + "cooling": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::cooling%]", + "delay_wash": "[%key:component::smartthings::entity::sensor::washer_job_state::state::delay_wash%]", + "drying": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::drying%]", + "finish": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::finish%]", + "freeze_protection": "[%key:component::smartthings::entity::sensor::washer_job_state::state::freeze_protection%]", + "none": "[%key:component::smartthings::entity::sensor::washer_job_state::state::none%]", + "pre_wash": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::pre_wash%]", + "rinse": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::rinse%]", + "spin": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::spin%]", + "wash": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::wash%]", + "weight_sensing": "[%key:component::smartthings::entity::sensor::washer_job_state::state::weight_sensing%]", + "wrinkle_prevent": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::state::wrinkle_prevent%]" + } + }, + "washer_sub_machine_state": { + "name": "Upper washer machine state", + "state": { + "pause": "[%key:common::state::paused%]", + "run": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]", + "stop": "[%key:common::state::stopped%]" + } + }, "water_consumption": { "name": "Water consumption" }, diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0f5961e0f3da8b..26fccc97148d09 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1387,12 +1387,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "dominos": { - "name": "Dominos Pizza", - "integration_type": "hub", - "config_flow": false, - "iot_class": "cloud_polling" - }, "doods": { "name": "DOODS - Dedicated Open Object Detection Service", "integration_type": "hub", diff --git a/homeassistant/helpers/template/__init__.py b/homeassistant/helpers/template/__init__.py index 7b61f56c85e682..a0fca2ed5ccb53 100644 --- a/homeassistant/helpers/template/__init__.py +++ b/homeassistant/helpers/template/__init__.py @@ -56,8 +56,6 @@ ) from homeassistant.exceptions import TemplateError from homeassistant.helpers import ( - area_registry as ar, - device_registry as dr, entity_registry as er, issue_registry as ir, location as loc_helper, @@ -78,7 +76,7 @@ template_context_manager, template_cv, ) -from .helpers import raise_no_default, resolve_area_id +from .helpers import raise_no_default from .render_info import RenderInfo, render_info_cv if TYPE_CHECKING: @@ -1244,103 +1242,6 @@ def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | N return None -def areas(hass: HomeAssistant) -> Iterable[str | None]: - """Return all areas.""" - return list(ar.async_get(hass).areas) - - -def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: - """Get the area ID from an area name, alias, device id, or entity id.""" - return resolve_area_id(hass, lookup_value) - - -def _get_area_name(area_reg: ar.AreaRegistry, valid_area_id: str) -> str: - """Get area name from valid area ID.""" - area = area_reg.async_get_area(valid_area_id) - assert area - return area.name - - -def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: - """Get the area name from an area id, device id, or entity id.""" - area_reg = ar.async_get(hass) - if area := area_reg.async_get_area(lookup_value): - return area.name - - dev_reg = dr.async_get(hass) - ent_reg = er.async_get(hass) - # Import here, not at top-level to avoid circular import - from homeassistant.helpers import config_validation as cv # noqa: PLC0415 - - try: - cv.entity_id(lookup_value) - except vol.Invalid: - pass - else: - if entity := ent_reg.async_get(lookup_value): - # If entity has an area ID, get the area name for that - if entity.area_id: - return _get_area_name(area_reg, entity.area_id) - # If entity has a device ID and the device exists with an area ID, get the - # area name for that - if ( - entity.device_id - and (device := dev_reg.async_get(entity.device_id)) - and device.area_id - ): - return _get_area_name(area_reg, device.area_id) - - if (device := dev_reg.async_get(lookup_value)) and device.area_id: - return _get_area_name(area_reg, device.area_id) - - return None - - -def area_entities(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: - """Return entities for a given area ID or name.""" - _area_id: str | None - # if area_name returns a value, we know the input was an ID, otherwise we - # assume it's a name, and if it's neither, we return early - if area_name(hass, area_id_or_name) is None: - _area_id = area_id(hass, area_id_or_name) - else: - _area_id = area_id_or_name - if _area_id is None: - return [] - ent_reg = er.async_get(hass) - entity_ids = [ - entry.entity_id for entry in er.async_entries_for_area(ent_reg, _area_id) - ] - dev_reg = dr.async_get(hass) - # We also need to add entities tied to a device in the area that don't themselves - # have an area specified since they inherit the area from the device. - entity_ids.extend( - [ - entity.entity_id - for device in dr.async_entries_for_area(dev_reg, _area_id) - for entity in er.async_entries_for_device(ent_reg, device.id) - if entity.area_id is None - ] - ) - return entity_ids - - -def area_devices(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: - """Return device IDs for a given area ID or name.""" - _area_id: str | None - # if area_name returns a value, we know the input was an ID, otherwise we - # assume it's a name, and if it's neither, we return early - if area_name(hass, area_id_or_name) is not None: - _area_id = area_id_or_name - else: - _area_id = area_id(hass, area_id_or_name) - if _area_id is None: - return [] - dev_reg = dr.async_get(hass) - entries = dr.async_entries_for_area(dev_reg, _area_id) - return [entry.id for entry in entries] - - def closest(hass: HomeAssistant, *args: Any) -> State | None: """Find closest entity. @@ -2182,6 +2083,7 @@ def __init__( ] = weakref.WeakValueDictionary() self.add_extension("jinja2.ext.loopcontrols") self.add_extension("jinja2.ext.do") + self.add_extension("homeassistant.helpers.template.extensions.AreaExtension") self.add_extension("homeassistant.helpers.template.extensions.Base64Extension") self.add_extension( "homeassistant.helpers.template.extensions.CollectionExtension" @@ -2276,22 +2178,6 @@ def wrapper(_: Any, *args: _P.args, **kwargs: _P.kwargs) -> _R: return jinja_context(wrapper) - # Area extensions - - self.globals["areas"] = hassfunction(areas) - - self.globals["area_id"] = hassfunction(area_id) - self.filters["area_id"] = self.globals["area_id"] - - self.globals["area_name"] = hassfunction(area_name) - self.filters["area_name"] = self.globals["area_name"] - - self.globals["area_entities"] = hassfunction(area_entities) - self.filters["area_entities"] = self.globals["area_entities"] - - self.globals["area_devices"] = hassfunction(area_devices) - self.filters["area_devices"] = self.globals["area_devices"] - # Integration extensions self.globals["integration_entities"] = hassfunction(integration_entities) diff --git a/homeassistant/helpers/template/extensions/__init__.py b/homeassistant/helpers/template/extensions/__init__.py index 02be2c1f35924e..9fdd8232c2a22f 100644 --- a/homeassistant/helpers/template/extensions/__init__.py +++ b/homeassistant/helpers/template/extensions/__init__.py @@ -1,5 +1,6 @@ """Home Assistant template extensions.""" +from .areas import AreaExtension from .base64 import Base64Extension from .collection import CollectionExtension from .crypto import CryptoExtension @@ -11,6 +12,7 @@ from .string import StringExtension __all__ = [ + "AreaExtension", "Base64Extension", "CollectionExtension", "CryptoExtension", diff --git a/homeassistant/helpers/template/extensions/areas.py b/homeassistant/helpers/template/extensions/areas.py new file mode 100644 index 00000000000000..1640243bb100ed --- /dev/null +++ b/homeassistant/helpers/template/extensions/areas.py @@ -0,0 +1,159 @@ +"""Area functions for Home Assistant templates.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TYPE_CHECKING + +import voluptuous as vol + +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) +from homeassistant.helpers.template.helpers import resolve_area_id + +from .base import BaseTemplateExtension, TemplateFunction + +if TYPE_CHECKING: + from homeassistant.helpers.template import TemplateEnvironment + + +class AreaExtension(BaseTemplateExtension): + """Extension for area-related template functions.""" + + def __init__(self, environment: TemplateEnvironment) -> None: + """Initialize the area extension.""" + super().__init__( + environment, + functions=[ + TemplateFunction( + "areas", + self.areas, + as_global=True, + requires_hass=True, + ), + TemplateFunction( + "area_id", + self.area_id, + as_global=True, + as_filter=True, + requires_hass=True, + limited_ok=False, + ), + TemplateFunction( + "area_name", + self.area_name, + as_global=True, + as_filter=True, + requires_hass=True, + limited_ok=False, + ), + TemplateFunction( + "area_entities", + self.area_entities, + as_global=True, + as_filter=True, + requires_hass=True, + ), + TemplateFunction( + "area_devices", + self.area_devices, + as_global=True, + as_filter=True, + requires_hass=True, + ), + ], + ) + + def areas(self) -> Iterable[str | None]: + """Return all areas.""" + return list(ar.async_get(self.hass).areas) + + def area_id(self, lookup_value: str) -> str | None: + """Get the area ID from an area name, alias, device id, or entity id.""" + return resolve_area_id(self.hass, lookup_value) + + def _get_area_name(self, area_reg: ar.AreaRegistry, valid_area_id: str) -> str: + """Get area name from valid area ID.""" + area = area_reg.async_get_area(valid_area_id) + assert area + return area.name + + def area_name(self, lookup_value: str) -> str | None: + """Get the area name from an area id, device id, or entity id.""" + area_reg = ar.async_get(self.hass) + if area := area_reg.async_get_area(lookup_value): + return area.name + + dev_reg = dr.async_get(self.hass) + ent_reg = er.async_get(self.hass) + # Import here, not at top-level to avoid circular import + from homeassistant.helpers import config_validation as cv # noqa: PLC0415 + + try: + cv.entity_id(lookup_value) + except vol.Invalid: + pass + else: + if entity := ent_reg.async_get(lookup_value): + # If entity has an area ID, get the area name for that + if entity.area_id: + return self._get_area_name(area_reg, entity.area_id) + # If entity has a device ID and the device exists with an area ID, get the + # area name for that + if ( + entity.device_id + and (device := dev_reg.async_get(entity.device_id)) + and device.area_id + ): + return self._get_area_name(area_reg, device.area_id) + + if (device := dev_reg.async_get(lookup_value)) and device.area_id: + return self._get_area_name(area_reg, device.area_id) + + return None + + def area_entities(self, area_id_or_name: str) -> Iterable[str]: + """Return entities for a given area ID or name.""" + _area_id: str | None + # if area_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early + if self.area_name(area_id_or_name) is None: + _area_id = self.area_id(area_id_or_name) + else: + _area_id = area_id_or_name + if _area_id is None: + return [] + ent_reg = er.async_get(self.hass) + entity_ids = [ + entry.entity_id for entry in er.async_entries_for_area(ent_reg, _area_id) + ] + dev_reg = dr.async_get(self.hass) + # We also need to add entities tied to a device in the area that don't themselves + # have an area specified since they inherit the area from the device. + entity_ids.extend( + [ + entity.entity_id + for device in dr.async_entries_for_area(dev_reg, _area_id) + for entity in er.async_entries_for_device(ent_reg, device.id) + if entity.area_id is None + ] + ) + return entity_ids + + def area_devices(self, area_id_or_name: str) -> Iterable[str]: + """Return device IDs for a given area ID or name.""" + _area_id: str | None + # if area_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early + if self.area_name(area_id_or_name) is not None: + _area_id = area_id_or_name + else: + _area_id = self.area_id(area_id_or_name) + if _area_id is None: + return [] + dev_reg = dr.async_get(self.hass) + entries = dr.async_entries_for_area(dev_reg, _area_id) + return [entry.id for entry in entries] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 38e415afb74774..8dd922112fdc92 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -210,10 +210,6 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 -# rpds-py frequently updates cargo causing build failures -# No wheels upstream available for armhf & armv7 -rpds-py==0.26.0 - # Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI num2words==0.5.14 diff --git a/pylint/plugins/hass_enforce_class_module.py b/pylint/plugins/hass_enforce_class_module.py index 41c07819fe8463..13c25b203a1788 100644 --- a/pylint/plugins/hass_enforce_class_module.py +++ b/pylint/plugins/hass_enforce_class_module.py @@ -85,7 +85,6 @@ "alert", "automation", "counter", - "dominos", "input_boolean", "input_button", "input_datetime", diff --git a/requirements_all.txt b/requirements_all.txt index 98b48f600f4c57..fc84047fa9ccf3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1713,9 +1713,6 @@ pigpio==1.78 # homeassistant.components.pilight pilight==0.1.1 -# homeassistant.components.dominos -pizzapi==0.0.6 - # homeassistant.components.plex plexauth==0.0.6 @@ -2132,7 +2129,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.1.3 +pylamarzocco==2.2.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc22a0feb8af1b..acc6e7e7f93412 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1779,7 +1779,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.1.3 +pylamarzocco==2.2.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 82f9a81aa3170b..23b8a77bceac5c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -52,30 +52,11 @@ "include": INCLUDED_REQUIREMENTS_WHEELS, "markers": {}, }, - # Pandas has issues building on armhf, it is expected they - # will drop the platform in the near future (they consider it - # "flimsy" on 386). The following packages depend on pandas, - # so we comment them out. - "wheels_armhf": { - "exclude": {"env-canada", "noaa-coops", "pyezviz", "pykrakenapi"}, - "include": INCLUDED_REQUIREMENTS_WHEELS, - "markers": {}, - }, - "wheels_armv7": { - "exclude": set(), - "include": INCLUDED_REQUIREMENTS_WHEELS, - "markers": {}, - }, "wheels_amd64": { "exclude": set(), "include": INCLUDED_REQUIREMENTS_WHEELS, "markers": {}, }, - "wheels_i386": { - "exclude": set(), - "include": INCLUDED_REQUIREMENTS_WHEELS, - "markers": {}, - }, } URL_PIN = ( @@ -221,10 +202,6 @@ # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 -# rpds-py frequently updates cargo causing build failures -# No wheels upstream available for armhf & armv7 -rpds-py==0.26.0 - # Constraint num2words to 0.5.14 as 0.5.15 and 0.5.16 were removed from PyPI num2words==0.5.14 diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 1f112c11b94891..e55465923a4629 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -35,8 +35,6 @@ RUN \ case "${{BUILD_ARCH}}" in \ "aarch64") go2rtc_suffix='arm64' ;; \ - "armhf") go2rtc_suffix='armv6' ;; \ - "armv7") go2rtc_suffix='arm' ;; \ *) go2rtc_suffix=${{BUILD_ARCH}} ;; \ esac \ && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v{go2rtc}/go2rtc_linux_${{go2rtc_suffix}} --output /bin/go2rtc \ diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 7f51c3cc501641..4057b40453da8d 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -272,8 +272,6 @@ "abode": {"jaraco-abode": {"jaraco-net"}}, # https://github.com/coinbase/coinbase-advanced-py "coinbase": {"homeassistant": {"coinbase-advanced-py"}}, - # https://github.com/ggrammar/pizzapi - "dominos": {"homeassistant": {"pizzapi"}}, # https://github.com/u9n/dlms-cosem "dsmr": {"dsmr-parser": {"dlms-cosem"}}, # https://github.com/ChrisMandich/PyFlume # Fixed with >=0.7.1 diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index e35dd459701716..526f0363218e80 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -147,9 +147,7 @@ def mock_bluetooth(enable_bluetooth: None) -> None: @pytest.fixture def mock_ble_device() -> BLEDevice: """Return a mock BLE device.""" - return BLEDevice( - "00:00:00:00:00:00", "GS_GS012345", details={"path": "path"}, rssi=50 - ) + return BLEDevice("00:00:00:00:00:00", "GS_GS012345", details={"path": "path"}) @pytest.fixture diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index 62c4f6ed37b8b2..ab24c7e10e4d68 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch +from bleak.backends.device import BLEDevice from freezegun.api import FrozenDateTimeFactory from pylamarzocco.const import FirmwareType, ModelName from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful @@ -196,11 +197,27 @@ async def test_config_flow_entry_migration_downgrade( assert not await hass.config_entries.async_setup(entry.entry_id) +@pytest.mark.parametrize( + ("ble_device", "has_client"), + [ + (None, False), + ( + BLEDevice( + address="aa:bb:cc:dd:ee:ff", + name="name", + details={}, + ), + True, + ), + ], +) async def test_bluetooth_is_set_from_discovery( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_lamarzocco: MagicMock, mock_cloud_client: MagicMock, + ble_device: BLEDevice | None, + has_client: bool, ) -> None: """Check we can fill a device from discovery info.""" @@ -216,13 +233,17 @@ async def test_bluetooth_is_set_from_discovery( patch( "homeassistant.components.lamarzocco.LaMarzoccoMachine" ) as mock_machine_class, + patch( + "homeassistant.components.lamarzocco.async_ble_device_from_address", + return_value=ble_device, + ), ): mock_machine_class.return_value = mock_lamarzocco await async_init_integration(hass, mock_config_entry) discovery.assert_called_once() assert mock_machine_class.call_count == 1 _, kwargs = mock_machine_class.call_args - assert kwargs["bluetooth_client"] is not None + assert (kwargs["bluetooth_client"] is not None) == has_client assert mock_config_entry.data[CONF_MAC] == service_info.address assert mock_config_entry.data[CONF_TOKEN] == "token" @@ -314,6 +335,50 @@ async def test_device( assert device == snapshot +async def test_disconnect_on_stop( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_ble_device: BLEDevice, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we close the connection with the La Marzocco when Home Assistants stops.""" + mock_config_entry = MockConfigEntry( + title="My LaMarzocco", + domain=DOMAIN, + version=4, + data=USER_INPUT + | { + CONF_MAC: mock_ble_device.address, + CONF_TOKEN: "token", + CONF_INSTALLATION_KEY: MOCK_INSTALLATION_KEY, + }, + unique_id=mock_lamarzocco.serial_number, + ) + + with ( + patch( + "homeassistant.components.lamarzocco.async_ble_device_from_address", + return_value=mock_ble_device, + ), + patch( + "homeassistant.components.lamarzocco.LaMarzoccoBluetoothClient", + autospec=True, + ) as mock_bt_client_cls, + ): + mock_bt_client = mock_bt_client_cls.return_value + mock_bt_client.disconnect = AsyncMock() + + await async_init_integration(hass, mock_config_entry) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + mock_bt_client.disconnect.assert_awaited_once() + + async def test_websocket_reconnects_after_termination( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 417fd9b9bdd92a..7725b1fe098cbd 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -3103,6 +3103,54 @@ 'state': 'on', }) # --- +# name: test_all_entities[da_wm_wm_100002][binary_sensor.washer_upper_washer_remote_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.washer_upper_washer_remote_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Upper washer remote control', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sub_remote_control', + 'unique_id': 'C097276D-C8D4-0000-0000-000000000000_sub_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100002][binary_sensor.washer_upper_washer_remote_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washer Upper washer remote control', + }), + 'context': , + 'entity_id': 'binary_sensor.washer_upper_washer_remote_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_all_entities[ecobee_sensor][binary_sensor.child_bedroom_motion-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index 118f81a6843425..6dd349ad6d54ed 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -1,4 +1,126 @@ # serializer version: 1 +# name: test_all_entities[da_ac_rac_000003][select.clim_salon_dust_filter_alarm_threshold-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '180', + '300', + '500', + '700', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.clim_salon_dust_filter_alarm_threshold', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dust filter alarm threshold', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dust_filter_alarm', + 'unique_id': '1e3f7ca2-e005-e1a4-f6d7-bc231e3f7977_main_samsungce.dustFilterAlarm_alarmThreshold_alarmThreshold', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ac_rac_000003][select.clim_salon_dust_filter_alarm_threshold-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Clim Salon Dust filter alarm threshold', + 'options': list([ + '180', + '300', + '500', + '700', + ]), + }), + 'context': , + 'entity_id': 'select.clim_salon_dust_filter_alarm_threshold', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '500', + }) +# --- +# name: test_all_entities[da_ac_rac_01001][select.aire_dormitorio_principal_dust_filter_alarm_threshold-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '180', + '300', + '500', + '700', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.aire_dormitorio_principal_dust_filter_alarm_threshold', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dust filter alarm threshold', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dust_filter_alarm', + 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_samsungce.dustFilterAlarm_alarmThreshold_alarmThreshold', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ac_rac_01001][select.aire_dormitorio_principal_dust_filter_alarm_threshold-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Aire Dormitorio Principal Dust filter alarm threshold', + 'options': list([ + '180', + '300', + '500', + '700', + ]), + }), + 'context': , + 'entity_id': 'select.aire_dormitorio_principal_dust_filter_alarm_threshold', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '500', + }) +# --- # name: test_all_entities[da_ks_microwave_0101x][select.microwave_lamp-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 856515c8f14f2e..76a04d99ba45fc 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -14312,6 +14312,201 @@ 'state': 'run', }) # --- +# name: test_all_entities[da_wm_wm_100002][sensor.washer_upper_washer_completion_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_upper_washer_completion_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upper washer completion time', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'washer_sub_completion_time', + 'unique_id': 'C097276D-C8D4-0000-0000-000000000000_sub_washerOperatingState_completionTime_completionTime', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100002][sensor.washer_upper_washer_completion_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Washer Upper washer completion time', + }), + 'context': , + 'entity_id': 'sensor.washer_upper_washer_completion_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-11-14T03:10:39+00:00', + }) +# --- +# name: test_all_entities[da_wm_wm_100002][sensor.washer_upper_washer_job_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'air_wash', + 'ai_rinse', + 'ai_spin', + 'ai_wash', + 'cooling', + 'delay_wash', + 'drying', + 'finish', + 'none', + 'pre_wash', + 'rinse', + 'spin', + 'wash', + 'weight_sensing', + 'wrinkle_prevent', + 'freeze_protection', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_upper_washer_job_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upper washer job state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'washer_sub_job_state', + 'unique_id': 'C097276D-C8D4-0000-0000-000000000000_sub_washerOperatingState_washerJobState_washerJobState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100002][sensor.washer_upper_washer_job_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washer Upper washer job state', + 'options': list([ + 'air_wash', + 'ai_rinse', + 'ai_spin', + 'ai_wash', + 'cooling', + 'delay_wash', + 'drying', + 'finish', + 'none', + 'pre_wash', + 'rinse', + 'spin', + 'wash', + 'weight_sensing', + 'wrinkle_prevent', + 'freeze_protection', + ]), + }), + 'context': , + 'entity_id': 'sensor.washer_upper_washer_job_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_all_entities[da_wm_wm_100002][sensor.washer_upper_washer_machine_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'pause', + 'run', + 'stop', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_upper_washer_machine_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upper washer machine state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'washer_sub_machine_state', + 'unique_id': 'C097276D-C8D4-0000-0000-000000000000_sub_washerOperatingState_machineState_machineState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100002][sensor.washer_upper_washer_machine_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washer Upper washer machine state', + 'options': list([ + 'pause', + 'run', + 'stop', + ]), + }), + 'context': , + 'entity_id': 'sensor.washer_upper_washer_machine_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stop', + }) +# --- # name: test_all_entities[ecobee_sensor][sensor.child_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_select.py b/tests/components/smartthings/test_select.py index 3e1746331f9895..0095edc786c1b4 100644 --- a/tests/components/smartthings/test_select.py +++ b/tests/components/smartthings/test_select.py @@ -189,3 +189,34 @@ async def test_availability_at_start( """Test unavailable at boot.""" await setup_integration(hass, mock_config_entry) assert hass.states.get("select.dryer").state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000003"]) +async def test_select_option_as_integer( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test selecting an option represented as an integer.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.clim_salon_dust_filter_alarm_threshold") + assert state.state == "500" + assert all(isinstance(option, str) for option in state.attributes[ATTR_OPTIONS]) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.clim_salon_dust_filter_alarm_threshold", + ATTR_OPTION: "300", + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "1e3f7ca2-e005-e1a4-f6d7-bc231e3f7977", + Capability.SAMSUNG_CE_DUST_FILTER_ALARM, + Command.SET_ALARM_THRESHOLD, + MAIN, + argument=300, + ) diff --git a/tests/components/tuya/fixtures/kt_wxqdp6ecfkd78zzz.json b/tests/components/tuya/fixtures/kt_wxqdp6ecfkd78zzz.json new file mode 100644 index 00000000000000..243816e3b242a3 --- /dev/null +++ b/tests/components/tuya/fixtures/kt_wxqdp6ecfkd78zzz.json @@ -0,0 +1,152 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Mini-Split", + "category": "kt", + "product_id": "wxqdp6ecfkd78zzz", + "product_name": "Air Conditioner", + "online": true, + "sub": false, + "time_zone": "-07:00", + "active_time": "2024-07-06T03:42:10+00:00", + "create_time": "2024-07-06T03:42:10+00:00", + "update_time": "2024-07-06T03:42:10+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103/F", + "min": 160, + "max": 900, + "scale": 1, + "step": 10 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["cold", "hot", "wet", "wind", "auto"] + } + }, + "mode_eco": { + "type": "Boolean", + "value": {} + }, + "heat": { + "type": "Boolean", + "value": {} + }, + "light": { + "type": "Boolean", + "value": {} + }, + "lock": { + "type": "Boolean", + "value": {} + }, + "switch_horizontal": { + "type": "Boolean", + "value": {} + }, + "sleep": { + "type": "Boolean", + "value": {} + }, + "health": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103/F", + "min": 160, + "max": 900, + "scale": 1, + "step": 10 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103/F", + "min": -300, + "max": 1760, + "scale": 1, + "step": 10 + } + }, + "mode": { + "type": "Enum", + "value": { + "range": ["cold", "hot", "wet", "wind", "auto"] + } + }, + "mode_eco": { + "type": "Boolean", + "value": {} + }, + "heat": { + "type": "Boolean", + "value": {} + }, + "light": { + "type": "Boolean", + "value": {} + }, + "lock": { + "type": "Boolean", + "value": {} + }, + "power_consumption": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 255, + "scale": 0, + "step": 1 + } + }, + "switch_horizontal": { + "type": "Boolean", + "value": {} + }, + "sleep": { + "type": "Boolean", + "value": {} + }, + "health": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch": false, + "temp_set": 660, + "temp_current": 690, + "mode": "cold", + "mode_eco": false, + "heat": false, + "light": false, + "lock": false, + "power_consumption": 0, + "switch_horizontal": false, + "sleep": false, + "health": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index 23d6892294f7e7..a1624453ad329e 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -804,6 +804,89 @@ 'state': 'cool', }) # --- +# name: test_platform_setup_and_discovery[climate.mini_split-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + , + , + ]), + 'max_temp': 90.0, + 'min_temp': 16.0, + 'swing_modes': list([ + 'off', + 'horizontal', + ]), + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.mini_split', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.zzz87dkfce6pdqxwtk', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.mini_split-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 69.0, + 'friendly_name': 'Mini-Split', + 'hvac_modes': list([ + , + , + , + , + , + , + ]), + 'max_temp': 90.0, + 'min_temp': 16.0, + 'supported_features': , + 'swing_mode': 'off', + 'swing_modes': list([ + 'off', + 'horizontal', + ]), + 'target_temp_step': 1.0, + 'temperature': 66.0, + }), + 'context': , + 'entity_id': 'climate.mini_split', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[climate.mr_pure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1290,3 +1373,169 @@ 'state': 'heat_cool', }) # --- +# name: test_us_customary_system[climate.air_conditioner] + ReadOnlyDict({ + 'current_temperature': 72, + 'max_temp': 187, + 'min_temp': 61, + 'target_temp_step': 1.0, + 'temperature': 73, + }) +# --- +# name: test_us_customary_system[climate.anbau] + ReadOnlyDict({ + 'max_temp': 95, + 'min_temp': 41, + 'target_temp_step': 1.0, + }) +# --- +# name: test_us_customary_system[climate.bathroom_radiator] + ReadOnlyDict({ + 'current_temperature': 67, + 'max_temp': 158, + 'min_temp': 34, + 'target_temp_step': 0.5, + 'temperature': 54, + }) +# --- +# name: test_us_customary_system[climate.boiler_temperature_controller] + ReadOnlyDict({ + 'current_temperature': 136, + 'max_temp': 95, + 'min_temp': 45, + 'target_temp_step': 1.0, + }) +# --- +# name: test_us_customary_system[climate.clima_cucina] + ReadOnlyDict({ + 'current_temperature': 81, + 'max_temp': 95, + 'min_temp': 41, + 'target_temp_step': 1.0, + 'temperature': 77, + }) +# --- +# name: test_us_customary_system[climate.el_termostato_de_la_cocina] + ReadOnlyDict({ + 'current_temperature': 113, + 'max_temp': 45, + 'min_temp': 34, + 'target_temp_step': 0.5, + 'temperature': 40, + }) +# --- +# name: test_us_customary_system[climate.empore] + ReadOnlyDict({ + 'current_temperature': 66, + 'max_temp': 95, + 'min_temp': 41, + 'target_temp_step': 1.0, + 'temperature': 95, + }) +# --- +# name: test_us_customary_system[climate.floor_thermostat_kitchen] + ReadOnlyDict({ + 'current_temperature': 68, + 'max_temp': 95, + 'min_temp': 41, + 'target_temp_step': 1.0, + 'temperature': 36, + }) +# --- +# name: test_us_customary_system[climate.geti_solar_pv_water_heater] + ReadOnlyDict({ + 'current_temperature': 140, + 'max_temp': 95, + 'min_temp': 45, + 'target_temp_step': 1.0, + }) +# --- +# name: test_us_customary_system[climate.kabinet] + ReadOnlyDict({ + 'current_temperature': 67, + 'max_temp': 203, + 'min_temp': 41, + 'target_temp_step': 0.5, + 'temperature': 71, + }) +# --- +# name: test_us_customary_system[climate.master_bedroom_ac] + ReadOnlyDict({ + 'current_temperature': 79, + 'max_temp': 190, + 'min_temp': 61, + 'target_temp_step': 0.5, + 'temperature': 167, + }) +# --- +# name: test_us_customary_system[climate.mini_split] + ReadOnlyDict({ + 'current_temperature': 156, + 'max_temp': 194, + 'min_temp': 61, + 'target_temp_step': 1.0, + 'temperature': 151, + }) +# --- +# name: test_us_customary_system[climate.mr_pure] + ReadOnlyDict({ + 'current_temperature': None, + 'max_temp': 95, + 'min_temp': 45, + 'target_temp_step': 1.0, + }) +# --- +# name: test_us_customary_system[climate.polotentsosushitel] + ReadOnlyDict({ + 'current_temperature': 78, + 'max_temp': 104, + 'min_temp': 41, + 'target_temp_step': 0.5, + 'temperature': 41, + }) +# --- +# name: test_us_customary_system[climate.salon] + ReadOnlyDict({ + 'current_temperature': 69, + 'max_temp': 43, + 'min_temp': 32, + 'target_temp_step': 0.5, + 'temperature': 43, + }) +# --- +# name: test_us_customary_system[climate.smart_thermostats] + ReadOnlyDict({ + 'current_temperature': 71, + 'max_temp': 194, + 'min_temp': 41, + 'target_temp_step': 1.0, + 'temperature': 54, + }) +# --- +# name: test_us_customary_system[climate.sove] + ReadOnlyDict({ + 'current_temperature': 75, + 'max_temp': 187, + 'min_temp': 61, + 'target_temp_step': 1.0, + 'temperature': 61, + }) +# --- +# name: test_us_customary_system[climate.term_prizemi] + ReadOnlyDict({ + 'current_temperature': 73, + 'max_temp': 158, + 'min_temp': 33, + 'target_temp_step': 0.1, + 'temperature': 73, + }) +# --- +# name: test_us_customary_system[climate.wifi_smart_gas_boiler_thermostat] + ReadOnlyDict({ + 'current_temperature': 77, + 'max_temp': 95, + 'min_temp': 41, + 'target_temp_step': 0.5, + 'temperature': 72, + }) +# --- diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 019a7957107009..fb657f244347fd 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -8462,3 +8462,34 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[zzz87dkfce6pdqxwtk] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'zzz87dkfce6pdqxwtk', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Air Conditioner', + 'model_id': 'wxqdp6ecfkd78zzz', + 'name': 'Mini-Split', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index b2b0d1fe2f9a1c..10930bd12a7af9 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -2399,6 +2399,63 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[light.mini_split_backlight-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.mini_split_backlight', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Backlight', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'backlight', + 'unique_id': 'tuya.zzz87dkfce6pdqxwtklight', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.mini_split_backlight-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Mini-Split Backlight', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.mini_split_backlight', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[light.parker_ceiling_fan_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 2dce257feb0512..cbd0033a630afd 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -6627,6 +6627,54 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[switch.mini_split_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.mini_split_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': 'tuya.zzz87dkfce6pdqxwtklock', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.mini_split_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mini-Split Child lock', + }), + 'context': , + 'entity_id': 'switch.mini_split_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[switch.mirilla_puerta_flip-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/test_climate.py b/tests/components/tuya/test_climate.py index 5cfa7dfb5b124e..53c808bda68cb4 100644 --- a/tests/components/tuya/test_climate.py +++ b/tests/components/tuya/test_climate.py @@ -7,13 +7,18 @@ import pytest from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from tuya_sharing import CustomerDevice, Manager from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODE, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, ATTR_PRESET_MODE, + ATTR_TARGET_TEMP_STEP, ATTR_TEMPERATURE, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, @@ -29,6 +34,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import entity_registry as er +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from . import initialize_entry @@ -50,6 +56,34 @@ async def test_platform_setup_and_discovery( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@patch("homeassistant.components.tuya.PLATFORMS", [Platform.CLIMATE]) +async def test_us_customary_system( + hass: HomeAssistant, + mock_manager: Manager, + mock_config_entry: MockConfigEntry, + mock_devices: list[CustomerDevice], + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test platform setup and discovery.""" + hass.config.units = US_CUSTOMARY_SYSTEM + + await initialize_entry(hass, mock_manager, mock_config_entry, mock_devices) + + for entity in entity_registry.entities.values(): + state = hass.states.get(entity.entity_id) + assert state.attributes == snapshot( + name=entity.entity_id, + include=props( + ATTR_CURRENT_TEMPERATURE, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TARGET_TEMP_STEP, + ATTR_TEMPERATURE, + ), + ) + + @pytest.mark.parametrize( ("mock_device_code", "entity_id", "service", "service_data", "expected_commands"), [ diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index f801050d815b43..f74e5f76246819 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -3259,7 +3259,7 @@ async def test_extract_from_target( ) -> None: """Test extract_from_target command with mixed target types including entities, devices, areas, and labels.""" - async def call_command(target: dict[str, str]) -> Any: + async def call_command(target: dict[str, list[str]]) -> Any: await websocket_client.send_json_auto_id( {"type": "extract_from_target", "target": target} ) diff --git a/tests/helpers/template/extensions/test_areas.py b/tests/helpers/template/extensions/test_areas.py new file mode 100644 index 00000000000000..d0ee66c1ce936f --- /dev/null +++ b/tests/helpers/template/extensions/test_areas.py @@ -0,0 +1,313 @@ +"""Test area template functions.""" + +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) + +from tests.common import MockConfigEntry +from tests.helpers.template.helpers import assert_result_info, render_to_info + + +async def test_areas(hass: HomeAssistant, area_registry: ar.AreaRegistry) -> None: + """Test areas function.""" + # Test no areas + info = render_to_info(hass, "{{ areas() }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test one area + area1 = area_registry.async_get_or_create("area1") + info = render_to_info(hass, "{{ areas() }}") + assert_result_info(info, [area1.id]) + assert info.rate_limit is None + + # Test multiple areas + area2 = area_registry.async_get_or_create("area2") + info = render_to_info(hass, "{{ areas() }}") + assert_result_info(info, [area1.id, area2.id]) + assert info.rate_limit is None + + +async def test_area_id( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test area_id function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing entity id + info = render_to_info(hass, "{{ area_id('sensor.fake') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing device id (hex value) + info = render_to_info(hass, "{{ area_id('123abc') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing area name + info = render_to_info(hass, "{{ area_id('fake area name') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_id(56) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + area_registry.async_get_or_create("sensor.fake") + + # Test device with single entity, which has no area + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test device ID, entity ID and area name as input with area name that looks like + # a device ID. Try a filter too + area_entry_hex = area_registry.async_get_or_create("123abc") + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_id }}}}") + assert_result_info(info, area_entry_hex.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry_hex.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{area_entry_hex.name}') }}}}") + assert_result_info(info, area_entry_hex.id) + assert info.rate_limit is None + + # Test device ID, entity ID and area name as input with area name that looks like an + # entity ID + area_entry_entity_id = area_registry.async_get_or_create("sensor.fake") + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_entity_id.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_entity_id.id + ) + + info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_id('{area_entry_entity_id.name}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + # Make sure that when entity doesn't have an area but its device does, that's what + # gets returned + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_entity_id.id + ) + + info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry_entity_id.id) + assert info.rate_limit is None + + +async def test_area_name( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test area_name function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing entity id + info = render_to_info(hass, "{{ area_name('sensor.fake') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing device id (hex value) + info = render_to_info(hass, "{{ area_name('123abc') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing area id + info = render_to_info(hass, "{{ area_name('1234567890') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_name(56) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test device with single entity, which has no area + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + info = render_to_info(hass, f"{{{{ area_name('{device_entry.id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test device ID, entity ID and area id as input. Try a filter too + area_entry = area_registry.async_get_or_create("123abc") + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry.id + ) + + info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_name }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ area_name('{area_entry.id}') }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + # Make sure that when entity doesn't have an area but its device does, that's what + # gets returned + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=None + ) + + info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") + assert_result_info(info, area_entry.name) + assert info.rate_limit is None + + +async def test_area_entities( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test area_entities function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing device id + info = render_to_info(hass, "{{ area_entities('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_entities(56) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + area_entry = area_registry.async_get_or_create("sensor.fake") + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + ) + entity_registry.async_update_entity(entity_entry.entity_id, area_id=area_entry.id) + + info = render_to_info(hass, f"{{{{ area_entities('{area_entry.id}') }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + # Test for entities that inherit area from device + device_entry = device_registry.async_get_or_create( + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + config_entry_id=config_entry.entry_id, + suggested_area="sensor.fake", + ) + entity_registry.async_get_or_create( + "light", + "hue_light", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") + assert_result_info(info, ["light.hue_5678", "light.hue_light_5678"]) + assert info.rate_limit is None + + +async def test_area_devices( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test area_devices function.""" + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + + # Test non existing device id + info = render_to_info(hass, "{{ area_devices('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_devices(56) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + area_entry = area_registry.async_get_or_create("sensor.fake") + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + suggested_area=area_entry.name, + ) + + info = render_to_info(hass, f"{{{{ area_devices('{area_entry.id}') }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_devices }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None diff --git a/tests/helpers/template/test_init.py b/tests/helpers/template/test_init.py index faf1481faffb2e..48317703d3fe5d 100644 --- a/tests/helpers/template/test_init.py +++ b/tests/helpers/template/test_init.py @@ -36,8 +36,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import ( - area_registry as ar, - device_registry as dr, entity, entity_registry as er, issue_registry as ir, @@ -2636,306 +2634,6 @@ async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> N assert info.rate_limit is None -async def test_areas(hass: HomeAssistant, area_registry: ar.AreaRegistry) -> None: - """Test areas function.""" - # Test no areas - info = render_to_info(hass, "{{ areas() }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test one area - area1 = area_registry.async_get_or_create("area1") - info = render_to_info(hass, "{{ areas() }}") - assert_result_info(info, [area1.id]) - assert info.rate_limit is None - - # Test multiple areas - area2 = area_registry.async_get_or_create("area2") - info = render_to_info(hass, "{{ areas() }}") - assert_result_info(info, [area1.id, area2.id]) - assert info.rate_limit is None - - -async def test_area_id( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test area_id function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing entity id - info = render_to_info(hass, "{{ area_id('sensor.fake') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing device id (hex value) - info = render_to_info(hass, "{{ area_id('123abc') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing area name - info = render_to_info(hass, "{{ area_id('fake area name') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_id(56) }}") - assert_result_info(info, None) - assert info.rate_limit is None - - area_entry_entity_id = area_registry.async_get_or_create("sensor.fake") - - # Test device with single entity, which has no area - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "5678", - config_entry=config_entry, - device_id=device_entry.id, - ) - info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test device ID, entity ID and area name as input with area name that looks like - # a device ID. Try a filter too - area_entry_hex = area_registry.async_get_or_create("123abc") - device_entry = device_registry.async_update_device( - device_entry.id, area_id=area_entry_hex.id - ) - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry_hex.id - ) - - info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_id }}}}") - assert_result_info(info, area_entry_hex.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry_hex.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{area_entry_hex.name}') }}}}") - assert_result_info(info, area_entry_hex.id) - assert info.rate_limit is None - - # Test device ID, entity ID and area name as input with area name that looks like an - # entity ID - area_entry_entity_id = area_registry.async_get_or_create("sensor.fake") - device_entry = device_registry.async_update_device( - device_entry.id, area_id=area_entry_entity_id.id - ) - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry_entity_id.id - ) - - info = render_to_info(hass, f"{{{{ area_id('{device_entry.id}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_id('{area_entry_entity_id.name}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - # Make sure that when entity doesn't have an area but its device does, that's what - # gets returned - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry_entity_id.id - ) - - info = render_to_info(hass, f"{{{{ area_id('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry_entity_id.id) - assert info.rate_limit is None - - -async def test_area_name( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test area_name function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing entity id - info = render_to_info(hass, "{{ area_name('sensor.fake') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing device id (hex value) - info = render_to_info(hass, "{{ area_name('123abc') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test non existing area id - info = render_to_info(hass, "{{ area_name('1234567890') }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_name(56) }}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test device with single entity, which has no area - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "5678", - config_entry=config_entry, - device_id=device_entry.id, - ) - info = render_to_info(hass, f"{{{{ area_name('{device_entry.id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") - assert_result_info(info, None) - assert info.rate_limit is None - - # Test device ID, entity ID and area id as input. Try a filter too - area_entry = area_registry.async_get_or_create("123abc") - device_entry = device_registry.async_update_device( - device_entry.id, area_id=area_entry.id - ) - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=area_entry.id - ) - - info = render_to_info(hass, f"{{{{ '{device_entry.id}' | area_name }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ area_name('{area_entry.id}') }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - # Make sure that when entity doesn't have an area but its device does, that's what - # gets returned - entity_entry = entity_registry.async_update_entity( - entity_entry.entity_id, area_id=None - ) - - info = render_to_info(hass, f"{{{{ area_name('{entity_entry.entity_id}') }}}}") - assert_result_info(info, area_entry.name) - assert info.rate_limit is None - - -async def test_area_entities( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test area_entities function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing device id - info = render_to_info(hass, "{{ area_entities('deadbeef') }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_entities(56) }}") - assert_result_info(info, []) - assert info.rate_limit is None - - area_entry = area_registry.async_get_or_create("sensor.fake") - entity_entry = entity_registry.async_get_or_create( - "light", - "hue", - "5678", - config_entry=config_entry, - ) - entity_registry.async_update_entity(entity_entry.entity_id, area_id=area_entry.id) - - info = render_to_info(hass, f"{{{{ area_entities('{area_entry.id}') }}}}") - assert_result_info(info, ["light.hue_5678"]) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") - assert_result_info(info, ["light.hue_5678"]) - assert info.rate_limit is None - - # Test for entities that inherit area from device - device_entry = device_registry.async_get_or_create( - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - config_entry_id=config_entry.entry_id, - suggested_area="sensor.fake", - ) - entity_registry.async_get_or_create( - "light", - "hue_light", - "5678", - config_entry=config_entry, - device_id=device_entry.id, - ) - - info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") - assert_result_info(info, ["light.hue_5678", "light.hue_light_5678"]) - assert info.rate_limit is None - - -async def test_area_devices( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, -) -> None: - """Test area_devices function.""" - config_entry = MockConfigEntry(domain="light") - config_entry.add_to_hass(hass) - - # Test non existing device id - info = render_to_info(hass, "{{ area_devices('deadbeef') }}") - assert_result_info(info, []) - assert info.rate_limit is None - - # Test wrong value type - info = render_to_info(hass, "{{ area_devices(56) }}") - assert_result_info(info, []) - assert info.rate_limit is None - - area_entry = area_registry.async_get_or_create("sensor.fake") - device_entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - suggested_area=area_entry.name, - ) - - info = render_to_info(hass, f"{{{{ area_devices('{area_entry.id}') }}}}") - assert_result_info(info, [device_entry.id]) - assert info.rate_limit is None - - info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_devices }}}}") - assert_result_info(info, [device_entry.id]) - assert info.rate_limit is None - - def test_closest_function_to_coord(hass: HomeAssistant) -> None: """Test closest function to coord.""" hass.states.async_set(