diff --git a/homeassistant/components/airos/manifest.json b/homeassistant/components/airos/manifest.json index ea73af579595a..de5609a1f6534 100644 --- a/homeassistant/components/airos/manifest.json +++ b/homeassistant/components/airos/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["airos==0.5.6"] + "requirements": ["airos==0.6.0"] } diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index f4e1257e36b64..ca3841dc9617e 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -200,7 +200,13 @@ def name(self) -> str: """Name of this state.""" return "" - variables = {"this": DummyState()} + # Render the current variables and add a dummy this variable to them. + variables = ( + self._run_variables + if isinstance(self._run_variables, dict) + else self._run_variables.async_render(self.hass, {}) + ) + variables = {"this": DummyState(), **variables} # Try to render the name as it can influence the entity ID self._attr_name = None diff --git a/homeassistant/components/vicare/entity.py b/homeassistant/components/vicare/entity.py index 8de008f602ce7..6dd986c3e2414 100644 --- a/homeassistant/components/vicare/entity.py +++ b/homeassistant/components/vicare/entity.py @@ -29,7 +29,6 @@ def __init__( gateway_serial = device_config.getConfig().serial device_id = device_config.getId() model = device_config.getModel().replace("_", " ") - via_device_identifier: tuple[str, str] = ("", "") identifier = ( f"{gateway_serial}_{device_serial.replace('-', '_')}" @@ -37,11 +36,6 @@ def __init__( else f"{gateway_serial}_{device_id}" ) - if device_serial is not None and device_serial.startswith("zigbee-"): - parts = device_serial.split("-") - if len(parts) == 3: # expect format zigbee-- - via_device_identifier = (DOMAIN, f"{gateway_serial}_zigbee_{parts[1]}") - self._api: PyViCareDevice | PyViCareHeatingDeviceComponent = ( component if component else device ) @@ -56,5 +50,13 @@ def __init__( manufacturer="Viessmann", model=model, configuration_url=VIESSMANN_DEVELOPER_PORTAL, - via_device=via_device_identifier, ) + + if device_serial and device_serial.startswith("zigbee-"): + parts = device_serial.split("-", 2) + if len(parts) == 3: + _, zigbee_ieee, _ = parts + self._attr_device_info["via_device"] = ( + DOMAIN, + f"{gateway_serial}_zigbee_{zigbee_ieee}", + ) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 6f97875247a43..82764f88019f1 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -89,6 +89,18 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM unit_getter: Callable[[PyViCareDevice], str | None] | None = None +SUPPLY_TEMPERATURE_SENSOR: ViCareSensorEntityDescription = ( + ViCareSensorEntityDescription( + key="supply_temperature", + translation_key="supply_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_getter=lambda api: api.getSupplyTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ) +) + + GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="outside_temperature", @@ -978,17 +990,11 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_getter=lambda api: api.getHydraulicSeparatorTemperature(), ), + SUPPLY_TEMPERATURE_SENSOR, ) CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( - ViCareSensorEntityDescription( - key="supply_temperature", - translation_key="supply_temperature", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_getter=lambda api: api.getSupplyTemperature(), - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), + SUPPLY_TEMPERATURE_SENSOR, ) BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/victron_remote_monitoring/energy.py b/homeassistant/components/victron_remote_monitoring/energy.py new file mode 100644 index 0000000000000..b3209703115d3 --- /dev/null +++ b/homeassistant/components/victron_remote_monitoring/energy.py @@ -0,0 +1,21 @@ +"""Victron Remote Monitoring energy platform.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + + +async def async_get_solar_forecast( + hass: HomeAssistant, config_entry_id: str +) -> dict[str, dict[str, float | int]] | None: + """Get solar forecast for a config entry ID.""" + if ( + entry := hass.config_entries.async_get_entry(config_entry_id) + ) is None or entry.state != ConfigEntryState.LOADED: + return None + data = entry.runtime_data.data.solar + if data is None: + return None + + return {"wh_hours": data.get_dict_isoformat} diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index 8c2161097312a..a1e6481da0b7f 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -74,6 +74,7 @@ async def authenticate( not appliances_manager.aircons and not appliances_manager.washers and not appliances_manager.dryers + and not appliances_manager.ovens ): return "no_appliances" diff --git a/homeassistant/components/whirlpool/entity.py b/homeassistant/components/whirlpool/entity.py index 95a065db2ca1d..7c3007cba20a3 100644 --- a/homeassistant/components/whirlpool/entity.py +++ b/homeassistant/components/whirlpool/entity.py @@ -3,6 +3,7 @@ import logging from whirlpool.appliance import Appliance +from whirlpool.oven import Cavity as OvenCavity, Oven from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -64,3 +65,31 @@ def _check_service_request(result: bool) -> None: translation_domain=DOMAIN, translation_key="request_failed", ) + + +class WhirlpoolOvenEntity(WhirlpoolEntity): + """Base class for Whirlpool oven entities.""" + + _appliance: Oven + + def __init__( + self, + appliance: Oven, + cavity: OvenCavity, + translation_key_base: str | None, + unique_id_suffix: str = "", + ) -> None: + """Initialize the entity.""" + self.cavity = cavity + cavity_suffix = "" + if appliance.get_oven_cavity_exists( + OvenCavity.Upper + ) and appliance.get_oven_cavity_exists(OvenCavity.Lower): + if cavity == OvenCavity.Upper: + cavity_suffix = "_upper" + elif cavity == OvenCavity.Lower: + cavity_suffix = "_lower" + super().__init__( + appliance, unique_id_suffix=f"{unique_id_suffix}{cavity_suffix}" + ) + self._attr_translation_key = f"{translation_key_base}{cavity_suffix}" diff --git a/homeassistant/components/whirlpool/icons.json b/homeassistant/components/whirlpool/icons.json index 574b491090e79..0a82ac349df30 100644 --- a/homeassistant/components/whirlpool/icons.json +++ b/homeassistant/components/whirlpool/icons.json @@ -6,6 +6,15 @@ }, "dryer_state": { "default": "mdi:tumble-dryer" + }, + "oven_state": { + "default": "mdi:stove" + }, + "oven_state_upper": { + "default": "mdi:stove" + }, + "oven_state_lower": { + "default": "mdi:stove" } } } diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index 545ae67eaa13f..91705428945e4 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -8,6 +8,12 @@ from whirlpool.appliance import Appliance from whirlpool.dryer import Dryer, MachineState as DryerMachineState +from whirlpool.oven import ( + Cavity as OvenCavity, + CavityState as OvenCavityState, + CookMode, + Oven, +) from whirlpool.washer import MachineState as WasherMachineState, Washer from homeassistant.components.sensor import ( @@ -15,14 +21,16 @@ SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) +from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow from . import WhirlpoolConfigEntry -from .entity import WhirlpoolEntity +from .entity import WhirlpoolEntity, WhirlpoolOvenEntity PARALLEL_UPDATES = 1 SCAN_INTERVAL = timedelta(minutes=5) @@ -88,6 +96,23 @@ STATE_CYCLE_SPINNING = "cycle_spinning" STATE_CYCLE_WASHING = "cycle_washing" +OVEN_CAVITY_STATE = { + OvenCavityState.Standby: "standby", + OvenCavityState.Preheating: "preheating", + OvenCavityState.Cooking: "cooking", +} + +OVEN_COOK_MODE = { + CookMode.Standby: "standby", + CookMode.Bake: "bake", + CookMode.ConvectBake: "convection_bake", + CookMode.Broil: "broil", + CookMode.ConvectBroil: "convection_broil", + CookMode.ConvectRoast: "convection_roast", + CookMode.KeepWarm: "keep_warm", + CookMode.AirFry: "air_fry", +} + def washer_state(washer: Washer) -> str | None: """Determine correct states for a washer.""" @@ -183,6 +208,59 @@ class WhirlpoolSensorEntityDescription(SensorEntityDescription): ) +@dataclass(frozen=True, kw_only=True) +class WhirlpoolOvenCavitySensorEntityDescription(SensorEntityDescription): + """Describes a Whirlpool oven cavity sensor entity.""" + + value_fn: Callable[[Oven, OvenCavity], str | int | float | None] + + +OVEN_CAVITY_SENSORS: tuple[WhirlpoolOvenCavitySensorEntityDescription, ...] = ( + WhirlpoolOvenCavitySensorEntityDescription( + key="oven_state", + translation_key="oven_state", + device_class=SensorDeviceClass.ENUM, + options=list(OVEN_CAVITY_STATE.values()), + value_fn=lambda oven, cavity: ( + OVEN_CAVITY_STATE.get(state) + if (state := oven.get_cavity_state(cavity)) is not None + else None + ), + ), + WhirlpoolOvenCavitySensorEntityDescription( + key="oven_cook_mode", + translation_key="oven_cook_mode", + device_class=SensorDeviceClass.ENUM, + options=list(OVEN_COOK_MODE.values()), + value_fn=lambda oven, cavity: ( + OVEN_COOK_MODE.get(cook_mode) + if (cook_mode := oven.get_cook_mode(cavity)) is not None + else None + ), + ), + WhirlpoolOvenCavitySensorEntityDescription( + key="oven_current_temperature", + translation_key="oven_current_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda oven, cavity: ( + temp if (temp := oven.get_temp(cavity)) != 0 else None + ), + ), + WhirlpoolOvenCavitySensorEntityDescription( + key="oven_target_temperature", + translation_key="oven_target_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda oven, cavity: ( + temp if (temp := oven.get_target_temp(cavity)) != 0 else None + ), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: WhirlpoolConfigEntry, @@ -215,12 +293,28 @@ async def async_setup_entry( for description in WASHER_DRYER_TIME_SENSORS ] + oven_upper_cavity_sensors = [ + WhirlpoolOvenCavitySensor(oven, OvenCavity.Upper, description) + for oven in appliances_manager.ovens + if oven.get_oven_cavity_exists(OvenCavity.Upper) + for description in OVEN_CAVITY_SENSORS + ] + + oven_lower_cavity_sensors = [ + WhirlpoolOvenCavitySensor(oven, OvenCavity.Lower, description) + for oven in appliances_manager.ovens + if oven.get_oven_cavity_exists(OvenCavity.Lower) + for description in OVEN_CAVITY_SENSORS + ] + async_add_entities( [ *washer_sensors, *washer_time_sensors, *dryer_sensors, *dryer_time_sensors, + *oven_upper_cavity_sensors, + *oven_lower_cavity_sensors, ] ) @@ -333,3 +427,26 @@ def _is_machine_state_finished(self) -> bool: def _is_machine_state_running(self) -> bool: """Return true if the machine is in a running state.""" return self._appliance.get_machine_state() is DryerMachineState.RunningMainCycle + + +class WhirlpoolOvenCavitySensor(WhirlpoolOvenEntity, SensorEntity): + """A class for Whirlpool oven cavity sensors.""" + + def __init__( + self, + oven: Oven, + cavity: OvenCavity, + description: WhirlpoolOvenCavitySensorEntityDescription, + ) -> None: + """Initialize the oven cavity sensor.""" + super().__init__( + oven, cavity, description.translation_key, f"-{description.key}" + ) + self.entity_description: WhirlpoolOvenCavitySensorEntityDescription = ( + description + ) + + @property + def native_value(self) -> StateType: + """Return native value of sensor.""" + return self.entity_description.value_fn(self._appliance, self.cavity) diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index 3ab65d2e3aa93..aa0fb6ffe11c4 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -120,6 +120,87 @@ }, "end_time": { "name": "End time" + }, + "oven_state": { + "name": "State", + "state": { + "standby": "[%key:common::state::standby%]", + "preheating": "Preheating", + "cooking": "Cooking" + } + }, + "oven_state_upper": { + "name": "Upper oven state", + "state": { + "standby": "[%key:common::state::standby%]", + "preheating": "[%key:component::whirlpool::entity::sensor::oven_state::state::preheating%]", + "cooking": "[%key:component::whirlpool::entity::sensor::oven_state::state::cooking%]" + } + }, + "oven_state_lower": { + "name": "Lower oven state", + "state": { + "standby": "[%key:common::state::standby%]", + "preheating": "[%key:component::whirlpool::entity::sensor::oven_state::state::preheating%]", + "cooking": "[%key:component::whirlpool::entity::sensor::oven_state::state::cooking%]" + } + }, + "oven_cook_mode": { + "name": "Cook mode", + "state": { + "standby": "[%key:common::state::standby%]", + "bake": "Bake", + "convection_bake": "Convection bake", + "broil": "Broil", + "convection_broil": "Convection broil", + "convection_roast": "Convection roast", + "keep_warm": "Keep warm", + "air_fry": "Air fry" + } + }, + "oven_cook_mode_upper": { + "name": "Upper oven cook mode", + "state": { + "standby": "[%key:common::state::standby%]", + "bake": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::bake%]", + "convection_bake": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::convection_bake%]", + "broil": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::broil%]", + "convection_broil": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::convection_broil%]", + "convection_roast": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::convection_roast%]", + "keep_warm": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::keep_warm%]", + "air_fry": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::air_fry%]" + } + }, + "oven_cook_mode_lower": { + "name": "Lower oven cook mode", + "state": { + "standby": "[%key:common::state::standby%]", + "bake": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::bake%]", + "convection_bake": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::convection_bake%]", + "broil": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::broil%]", + "convection_broil": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::convection_broil%]", + "convection_roast": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::convection_roast%]", + "keep_warm": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::keep_warm%]", + "air_fry": "[%key:component::whirlpool::entity::sensor::oven_cook_mode::state::air_fry%]" + } + }, + "oven_current_temperature": { + "name": "Current temperature" + }, + "oven_current_temperature_upper": { + "name": "Upper oven current temperature" + }, + "oven_current_temperature_lower": { + "name": "Lower oven current temperature" + }, + "oven_target_temperature": { + "name": "Target temperature" + }, + "oven_target_temperature_upper": { + "name": "Upper oven target temperature" + }, + "oven_target_temperature_lower": { + "name": "Lower oven target temperature" } } }, diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 189abd6474dbd..25e68ae747d0d 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -58,7 +58,6 @@ selector, target as target_helpers, template, - translation, ) from .deprecation import deprecated_class, deprecated_function, deprecated_hass_argument from .selector import TargetSelector @@ -586,11 +585,6 @@ async def async_get_all_descriptions( _load_services_files, integrations ) - # Load translations for all service domains - translations = await translation.async_get_translations( - hass, "en", "services", services - ) - # Build response descriptions: dict[str, dict[str, Any]] = {} for domain, services_map in services.items(): @@ -617,40 +611,11 @@ async def async_get_all_descriptions( # Don't warn for missing services, because it triggers false # positives for things like scripts, that register as a service - # - # When name & description are in the translations use those; - # otherwise fallback to backwards compatible behavior from - # the time when we didn't have translations for descriptions yet. - # This mimics the behavior of the frontend. - description = { - "name": translations.get( - f"component.{domain}.services.{service_name}.name", - yaml_description.get("name", ""), - ), - "description": translations.get( - f"component.{domain}.services.{service_name}.description", - yaml_description.get("description", ""), - ), - "fields": dict(yaml_description.get("fields", {})), - } - - # Translate fields names & descriptions as well - for field_name, field_schema in description["fields"].items(): - if name := translations.get( - f"component.{domain}.services.{service_name}.fields.{field_name}.name" - ): - field_schema["name"] = name - if desc := translations.get( - f"component.{domain}.services.{service_name}.fields.{field_name}.description" - ): - field_schema["description"] = desc - if example := translations.get( - f"component.{domain}.services.{service_name}.fields.{field_name}.example" - ): - field_schema["example"] = example - - if "target" in yaml_description: - description["target"] = yaml_description["target"] + description = {"fields": yaml_description.get("fields", {})} + + for item in ("description", "name", "target"): + if item in yaml_description: + description[item] = yaml_description[item] response = service.supports_response if response is not SupportsResponse.NONE: diff --git a/requirements_all.txt b/requirements_all.txt index 6aafb505b7097..07c685d4186bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -459,7 +459,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.5.6 +airos==0.6.0 # homeassistant.components.airthings_ble airthings-ble==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51b09c4645751..13a5532b4f396 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -441,7 +441,7 @@ airgradient==0.9.2 airly==1.1.0 # homeassistant.components.airos -airos==0.5.6 +airos==0.6.0 # homeassistant.components.airthings_ble airthings-ble==1.1.1 diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr index 5977f9024346d..8c62ff96af58b 100644 --- a/tests/components/airos/snapshots/test_diagnostics.ambr +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -14,6 +14,7 @@ ]), 'derived': dict({ 'access_point': True, + 'fw_major': None, 'mac': '**REDACTED**', 'mac_interface': 'br0', 'mode': 'point_to_point', diff --git a/tests/components/api/snapshots/test_init.ambr b/tests/components/api/snapshots/test_init.ambr index d6277e7fc975c..45d8cada22ba8 100644 --- a/tests/components/api/snapshots/test_init.ambr +++ b/tests/components/api/snapshots/test_init.ambr @@ -5,18 +5,13 @@ 'domain': 'group', 'services': dict({ 'reload': dict({ - 'description': 'Reloads group configuration, entities, and notify services from YAML-configuration.', 'fields': dict({ }), - 'name': 'Reload', }), 'remove': dict({ - 'description': 'Removes a group.', 'fields': dict({ 'object_id': dict({ - 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', 'example': 'test_group', - 'name': 'Object ID', 'required': True, 'selector': dict({ 'object': dict({ @@ -25,15 +20,11 @@ }), }), }), - 'name': 'Remove', }), 'set': dict({ - 'description': 'Creates/Updates a group.', 'fields': dict({ 'add_entities': dict({ - 'description': 'List of members to be added to the group. Cannot be used in combination with `Entities` or `Remove entities`.', 'example': 'domain.entity_id1, domain.entity_id2', - 'name': 'Add entities', 'selector': dict({ 'entity': dict({ 'multiple': True, @@ -42,17 +33,13 @@ }), }), 'all': dict({ - 'description': 'Enable this option if the group should only be used when all entities are in state `on`.', - 'name': 'All', 'selector': dict({ 'boolean': dict({ }), }), }), 'entities': dict({ - 'description': 'List of all members in the group. Cannot be used in combination with `Add entities` or `Remove entities`.', 'example': 'domain.entity_id1, domain.entity_id2', - 'name': 'Entities', 'selector': dict({ 'entity': dict({ 'multiple': True, @@ -61,18 +48,14 @@ }), }), 'icon': dict({ - 'description': 'Name of the icon for the group.', 'example': 'mdi:camera', - 'name': 'Icon', 'selector': dict({ 'icon': dict({ }), }), }), 'name': dict({ - 'description': 'Name of the group.', 'example': 'My test group', - 'name': 'Name', 'selector': dict({ 'text': dict({ 'multiline': False, @@ -81,9 +64,7 @@ }), }), 'object_id': dict({ - 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', 'example': 'test_group', - 'name': 'Object ID', 'required': True, 'selector': dict({ 'text': dict({ @@ -93,9 +74,7 @@ }), }), 'remove_entities': dict({ - 'description': 'List of members to be removed from a group. Cannot be used in combination with `Entities` or `Add entities`.', 'example': 'domain.entity_id1, domain.entity_id2', - 'name': 'Remove entities', 'selector': dict({ 'entity': dict({ 'multiple': True, @@ -104,7 +83,6 @@ }), }), }), - 'name': 'Set', }), }), }), @@ -139,10 +117,8 @@ 'name': 'Translated name', }), 'set_level': dict({ - 'description': '', 'fields': dict({ }), - 'name': '', }), }), }) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index c000c1c318144..691d270f524bb 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -375,10 +375,6 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: "homeassistant.helpers.service._load_services_file", side_effect=_load_services_file, ), - patch( - "homeassistant.helpers.service.translation.async_get_translations", - return_value={}, - ), ): resp = await mock_api_client.get(const.URL_API_SERVICES) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index b5e5e18f6649b..909e32c4bf78b 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -864,7 +864,7 @@ async def test_translated_unit( """Test translated unit.""" with patch( - "homeassistant.helpers.service.translation.async_get_translations", + "homeassistant.helpers.entity_platform.translation.async_get_translations", return_value={ "component.test.entity.number.test_translation_key.unit_of_measurement": "Tests" }, @@ -896,7 +896,7 @@ async def test_translated_unit_with_native_unit_raises( """Test that translated unit.""" with patch( - "homeassistant.helpers.service.translation.async_get_translations", + "homeassistant.helpers.entity_platform.translation.async_get_translations", return_value={ "component.test.entity.number.test_translation_key.unit_of_measurement": "Tests" }, diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 908ec222e1318..ff068d6f95201 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -627,9 +627,6 @@ async def test_service_descriptions(hass: HomeAssistant) -> None: assert descriptions[DOMAIN]["test_name"]["name"] == "ABC" - # Test 4: verify that names from YAML are taken into account as well - assert descriptions[DOMAIN]["turn_on"]["name"] == "Turn on" - async def test_shared_context(hass: HomeAssistant) -> None: """Test that the shared context is passed down the chain.""" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 90f564464d2a4..3daa0903a0092 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -601,7 +601,7 @@ async def test_translated_unit( """Test translated unit.""" with patch( - "homeassistant.helpers.service.translation.async_get_translations", + "homeassistant.helpers.entity_platform.translation.async_get_translations", return_value={ "component.test.entity.sensor.test_translation_key.unit_of_measurement": "Tests" }, @@ -633,7 +633,7 @@ async def test_translated_unit_with_native_unit_raises( """Test that translated unit.""" with patch( - "homeassistant.helpers.service.translation.async_get_translations", + "homeassistant.helpers.entity_platform.translation.async_get_translations", return_value={ "component.test.entity.sensor.test_translation_key.unit_of_measurement": "Tests" }, @@ -664,7 +664,7 @@ async def test_unit_translation_key_without_platform_raises( """Test that unit translation key property raises if the entity has no platform yet.""" with patch( - "homeassistant.helpers.service.translation.async_get_translations", + "homeassistant.helpers.entity_platform.translation.async_get_translations", return_value={ "component.test.entity.sensor.test_translation_key.unit_of_measurement": "Tests" }, diff --git a/tests/components/template/test_blueprint.py b/tests/components/template/test_blueprint.py index fd45c3b008b3a..469e3df0ae06e 100644 --- a/tests/components/template/test_blueprint.py +++ b/tests/components/template/test_blueprint.py @@ -228,6 +228,90 @@ async def test_reload_template_when_blueprint_changes(hass: HomeAssistant) -> No assert not_inverted.state == "on" +async def test_init_attribute_variables_from_blueprint(hass: HomeAssistant) -> None: + """Test a state based blueprint initializes icon, name, and picture with variables.""" + blueprint = "test_init_attribute_variables.yaml" + source = "switch.foo" + entity_id = "sensor.foo" + hass.states.async_set(source, "on", {"friendly_name": "Foo"}) + config = { + DOMAIN: [ + { + "use_blueprint": { + "path": blueprint, + "input": {"switch": source}, + }, + } + ], + } + assert await async_setup_component( + hass, + DOMAIN, + config, + ) + await hass.async_block_till_done() + + # Check initial state + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == "True" + assert sensor.attributes["icon"] == "mdi:lightbulb" + assert sensor.attributes["entity_picture"] == "on.png" + assert sensor.attributes["friendly_name"] == "Foo" + assert sensor.attributes["extra"] == "ab" + + hass.states.async_set(source, "off", {"friendly_name": "Foo"}) + await hass.async_block_till_done() + + # Check to see that the template light works + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == "False" + assert sensor.attributes["icon"] == "mdi:lightbulb-off" + assert sensor.attributes["entity_picture"] == "off.png" + assert sensor.attributes["friendly_name"] == "Foo" + assert sensor.attributes["extra"] == "ab" + + # Reload the templates without any change, but with updated blueprint + blueprint_config = yaml_util.load_yaml( + pathlib.Path("tests/testing_config/blueprints/template/") / blueprint + ) + blueprint_config["variables"]["extraa"] = "c" + blueprint_config["sensor"]["variables"]["extrab"] = "d" + with ( + patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ), + patch( + "homeassistant.components.blueprint.models.yaml_util.load_yaml_dict", + autospec=True, + return_value=blueprint_config, + ), + ): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == "False" + assert sensor.attributes["icon"] == "mdi:lightbulb-off" + assert sensor.attributes["entity_picture"] == "off.png" + assert sensor.attributes["friendly_name"] == "Foo" + assert sensor.attributes["extra"] == "cd" + + hass.states.async_set(source, "on", {"friendly_name": "Foo"}) + await hass.async_block_till_done() + + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == "True" + assert sensor.attributes["icon"] == "mdi:lightbulb" + assert sensor.attributes["entity_picture"] == "on.png" + assert sensor.attributes["friendly_name"] == "Foo" + assert sensor.attributes["extra"] == "cd" + + @pytest.mark.parametrize( ("blueprint"), ["test_event_sensor.yaml", "test_event_sensor_legacy_schema.yaml"], diff --git a/tests/components/template/test_config.py b/tests/components/template/test_config.py index 88d6a2554f538..1238e1fcc16c3 100644 --- a/tests/components/template/test_config.py +++ b/tests/components/template/test_config.py @@ -5,6 +5,7 @@ import pytest import voluptuous as vol +from homeassistant.components.template import DOMAIN from homeassistant.components.template.config import ( CONFIG_SECTION_SCHEMA, async_validate_config_section, @@ -12,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.template import Template +from homeassistant.setup import async_setup_component @pytest.mark.parametrize( @@ -256,3 +258,59 @@ async def test_combined_trigger_variables( assert root_variables.as_dict() == expected_root variables: ScriptVariables = validated["binary_sensor"][0].get("variables", empty) assert variables.as_dict() == expected_entity + + +async def test_state_init_attribute_variables( + hass: HomeAssistant, +) -> None: + """Test a state based template entity initializes icon, name, and picture with variables.""" + source = "switch.foo" + entity_id = "sensor.foo" + + hass.states.async_set(source, "on", {"friendly_name": "Foo"}) + config = { + "template": [ + { + "variables": { + "switch": "switch.foo", + "on_icon": "mdi:lightbulb", + "on_picture": "on.png", + }, + "sensor": { + "variables": { + "off_icon": "mdi:lightbulb-off", + "off_picture": "off.png", + }, + "name": "{{ state_attr(switch, 'friendly_name') }}", + "icon": "{{ on_icon if is_state(switch, 'on') else off_icon }}", + "picture": "{{ on_picture if is_state(switch, 'on') else off_picture }}", + "state": "{{ is_state(switch, 'on') }}", + }, + } + ], + } + assert await async_setup_component( + hass, + DOMAIN, + config, + ) + await hass.async_block_till_done() + + # Check initial state + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == "True" + assert sensor.attributes["icon"] == "mdi:lightbulb" + assert sensor.attributes["entity_picture"] == "on.png" + assert sensor.attributes["friendly_name"] == "Foo" + + hass.states.async_set(source, "off", {"friendly_name": "Foo"}) + await hass.async_block_till_done() + + # Check to see that the template light works + sensor = hass.states.get(entity_id) + assert sensor + assert sensor.state == "False" + assert sensor.attributes["icon"] == "mdi:lightbulb-off" + assert sensor.attributes["entity_picture"] == "off.png" + assert sensor.attributes["friendly_name"] == "Foo" diff --git a/tests/components/vicare/snapshots/test_sensor.ambr b/tests/components/vicare/snapshots/test_sensor.ambr index 1f6d620c49899..1d2ff27bbe27c 100644 --- a/tests/components/vicare/snapshots/test_sensor.ambr +++ b/tests/components/vicare/snapshots/test_sensor.ambr @@ -3475,6 +3475,62 @@ 'state': '37', }) # --- +# name: test_all_entities[sensor.model9_supply_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model9_supply_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Supply temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'supply_temperature', + 'unique_id': 'gateway9_zigbee_################-supply_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.model9_supply_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model9 Supply temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model9_supply_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.0', + }) +# --- # name: test_all_entities[sensor.vitovalor_hydraulic_separator_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/victron_remote_monitoring/test_energy.py b/tests/components/victron_remote_monitoring/test_energy.py new file mode 100644 index 0000000000000..0eab17cdece36 --- /dev/null +++ b/tests/components/victron_remote_monitoring/test_energy.py @@ -0,0 +1,44 @@ +"""Test the Victron Remote Monitoring energy platform.""" + +from homeassistant.components.victron_remote_monitoring import energy +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_energy_solar_forecast( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test fetching the solar forecast for the energy dashboard.""" + config_entry = init_integration + + assert config_entry.state is ConfigEntryState.LOADED + + assert await energy.async_get_solar_forecast(hass, config_entry.entry_id) == { + "wh_hours": { + "2025-04-23T10:00:00+00:00": 5050.1, + "2025-04-23T11:00:00+00:00": 5000.2, + "2025-04-24T10:00:00+00:00": 2250.3, + "2025-04-24T11:00:00+00:00": 2000.4, + "2025-04-25T10:00:00+00:00": 1000.5, + "2025-04-25T11:00:00+00:00": 500.6, + } + } + + +async def test_energy_missing_entry(hass: HomeAssistant) -> None: + """Return None when config entry cannot be found.""" + assert await energy.async_get_solar_forecast(hass, "missing") is None + + +async def test_energy_no_solar_data( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Return None when the coordinator has no solar forecast data.""" + config_entry = init_integration + assert config_entry.state is ConfigEntryState.LOADED + + config_entry.runtime_data.data.solar = None + + assert await energy.async_get_solar_forecast(hass, config_entry.entry_id) is None diff --git a/tests/components/websocket_api/snapshots/test_commands.ambr b/tests/components/websocket_api/snapshots/test_commands.ambr index 3117eeeeb11cc..8b1a38aa12bef 100644 --- a/tests/components/websocket_api/snapshots/test_commands.ambr +++ b/tests/components/websocket_api/snapshots/test_commands.ambr @@ -2,18 +2,13 @@ # name: test_get_services dict({ 'reload': dict({ - 'description': 'Reloads group configuration, entities, and notify services from YAML-configuration.', 'fields': dict({ }), - 'name': 'Reload', }), 'remove': dict({ - 'description': 'Removes a group.', 'fields': dict({ 'object_id': dict({ - 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', 'example': 'test_group', - 'name': 'Object ID', 'required': True, 'selector': dict({ 'object': dict({ @@ -22,15 +17,11 @@ }), }), }), - 'name': 'Remove', }), 'set': dict({ - 'description': 'Creates/Updates a group.', 'fields': dict({ 'add_entities': dict({ - 'description': 'List of members to be added to the group. Cannot be used in combination with `Entities` or `Remove entities`.', 'example': 'domain.entity_id1, domain.entity_id2', - 'name': 'Add entities', 'selector': dict({ 'entity': dict({ 'multiple': True, @@ -39,17 +30,13 @@ }), }), 'all': dict({ - 'description': 'Enable this option if the group should only be used when all entities are in state `on`.', - 'name': 'All', 'selector': dict({ 'boolean': dict({ }), }), }), 'entities': dict({ - 'description': 'List of all members in the group. Cannot be used in combination with `Add entities` or `Remove entities`.', 'example': 'domain.entity_id1, domain.entity_id2', - 'name': 'Entities', 'selector': dict({ 'entity': dict({ 'multiple': True, @@ -58,18 +45,14 @@ }), }), 'icon': dict({ - 'description': 'Name of the icon for the group.', 'example': 'mdi:camera', - 'name': 'Icon', 'selector': dict({ 'icon': dict({ }), }), }), 'name': dict({ - 'description': 'Name of the group.', 'example': 'My test group', - 'name': 'Name', 'selector': dict({ 'text': dict({ 'multiline': False, @@ -78,9 +61,7 @@ }), }), 'object_id': dict({ - 'description': 'Object ID of this group. This object ID is used as part of the entity ID. Entity ID format: [domain].[object_id].', 'example': 'test_group', - 'name': 'Object ID', 'required': True, 'selector': dict({ 'text': dict({ @@ -90,9 +71,7 @@ }), }), 'remove_entities': dict({ - 'description': 'List of members to be removed from a group. Cannot be used in combination with `Entities` or `Add entities`.', 'example': 'domain.entity_id1, domain.entity_id2', - 'name': 'Remove entities', 'selector': dict({ 'entity': dict({ 'multiple': True, @@ -101,7 +80,6 @@ }), }), }), - 'name': 'Set', }), }) # --- @@ -132,10 +110,8 @@ 'name': 'Translated name', }), 'set_level': dict({ - 'description': '', 'fields': dict({ }), - 'name': '', }), }) # --- diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 480f44c0ff1e0..f801050d815b4 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -784,10 +784,6 @@ def _load_services_file(integration: Integration) -> JSON_TYPE: "homeassistant.helpers.service._load_services_file", side_effect=_load_services_file, ), - patch( - "homeassistant.helpers.service.translation.async_get_translations", - return_value={}, - ), ): await websocket_client.send_json_auto_id({"type": "get_services"}) msg = await websocket_client.receive_json() diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index fb82750924aa4..75ab793eeecea 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import Mock import pytest -from whirlpool import aircon, appliancesmanager, auth, dryer, washer +from whirlpool import aircon, appliancesmanager, auth, dryer, oven, washer from whirlpool.backendselector import Brand, Region from .const import MOCK_SAID1, MOCK_SAID2 @@ -49,7 +49,12 @@ def fixture_mock_auth_api(): @pytest.fixture(name="mock_appliances_manager_api", autouse=True) def fixture_mock_appliances_manager_api( - mock_aircon1_api, mock_aircon2_api, mock_washer_api, mock_dryer_api + mock_aircon1_api, + mock_aircon2_api, + mock_washer_api, + mock_dryer_api, + mock_oven_single_cavity_api, + mock_oven_dual_cavity_api, ): """Set up AppliancesManager fixture.""" with ( @@ -68,6 +73,10 @@ def fixture_mock_appliances_manager_api( ] mock_appliances_manager.return_value.washers = [mock_washer_api] mock_appliances_manager.return_value.dryers = [mock_dryer_api] + mock_appliances_manager.return_value.ovens = [ + mock_oven_single_cavity_api, + mock_oven_dual_cavity_api, + ] yield mock_appliances_manager @@ -155,3 +164,42 @@ def mock_dryer_api(): mock_dryer.get_time_remaining.return_value = 3540 mock_dryer.get_cycle_status_sensing.return_value = False return mock_dryer + + +@pytest.fixture +def mock_oven_single_cavity_api(): + """Get a mock of a single cavity oven.""" + mock_oven = Mock(spec=oven.Oven, said="said_oven_single") + mock_oven.name = "Single Cavity Oven" + mock_oven.appliance_info = Mock( + data_model="oven", category="oven", model_number="12345" + ) + mock_oven.get_cavity_state.return_value = oven.CavityState.Standby + mock_oven.get_cook_mode.return_value = oven.CookMode.Bake + mock_oven.get_online.return_value = True + mock_oven.get_oven_cavity_exists.side_effect = ( + lambda cavity: cavity == oven.Cavity.Upper + ) + mock_oven.get_temp.return_value = 180 + mock_oven.get_target_temp.return_value = 200 + return mock_oven + + +@pytest.fixture +def mock_oven_dual_cavity_api(): + """Get a mock of a dual cavity oven.""" + mock_oven = Mock(spec=oven.Oven, said="said_oven_dual") + mock_oven.name = "Dual Cavity Oven" + mock_oven.appliance_info = Mock( + data_model="oven", category="oven", model_number="12345" + ) + mock_oven.get_cavity_state.return_value = oven.CavityState.Standby + mock_oven.get_cook_mode.return_value = oven.CookMode.Bake + mock_oven.get_online.return_value = True + mock_oven.get_oven_cavity_exists.side_effect = lambda cavity: cavity in ( + oven.Cavity.Upper, + oven.Cavity.Lower, + ) + mock_oven.get_temp.return_value = 180 + mock_oven.get_target_temp.return_value = 200 + return mock_oven diff --git a/tests/components/whirlpool/snapshots/test_diagnostics.ambr b/tests/components/whirlpool/snapshots/test_diagnostics.ambr index 11aecc93d0da0..783e5e980ca08 100644 --- a/tests/components/whirlpool/snapshots/test_diagnostics.ambr +++ b/tests/components/whirlpool/snapshots/test_diagnostics.ambr @@ -22,6 +22,16 @@ }), }), 'ovens': dict({ + 'Dual Cavity Oven': dict({ + 'category': 'oven', + 'data_model': 'oven', + 'model_number': '12345', + }), + 'Single Cavity Oven': dict({ + 'category': 'oven', + 'data_model': 'oven', + 'model_number': '12345', + }), }), 'washers': dict({ 'Washer': dict({ diff --git a/tests/components/whirlpool/snapshots/test_sensor.ambr b/tests/components/whirlpool/snapshots/test_sensor.ambr index 64b513abe4e47..976adc3602529 100644 --- a/tests/components/whirlpool/snapshots/test_sensor.ambr +++ b/tests/components/whirlpool/snapshots/test_sensor.ambr @@ -145,6 +145,732 @@ 'state': 'running_maincycle', }) # --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_cook_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'bake', + 'convection_bake', + 'broil', + 'convection_broil', + 'convection_roast', + 'keep_warm', + 'air_fry', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_cook_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lower oven cook mode', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_cook_mode_lower', + 'unique_id': 'said_oven_dual-oven_cook_mode_lower', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_cook_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dual cavity oven Lower oven cook mode', + 'options': list([ + 'standby', + 'bake', + 'convection_bake', + 'broil', + 'convection_broil', + 'convection_roast', + 'keep_warm', + 'air_fry', + ]), + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_cook_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'bake', + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_current_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_current_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lower oven current temperature', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_current_temperature_lower', + 'unique_id': 'said_oven_dual-oven_current_temperature_lower', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_current_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Dual cavity oven Lower oven current temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '180', + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'preheating', + 'cooking', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_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': 'Lower oven state', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_state_lower', + 'unique_id': 'said_oven_dual-oven_state_lower', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dual cavity oven Lower oven state', + 'options': list([ + 'standby', + 'preheating', + 'cooking', + ]), + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'standby', + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_target_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_target_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lower oven target temperature', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_target_temperature_lower', + 'unique_id': 'said_oven_dual-oven_target_temperature_lower', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_lower_oven_target_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Dual cavity oven Lower oven target temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_lower_oven_target_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '200', + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_cook_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'bake', + 'convection_bake', + 'broil', + 'convection_broil', + 'convection_roast', + 'keep_warm', + 'air_fry', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_cook_mode', + '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 oven cook mode', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_cook_mode_upper', + 'unique_id': 'said_oven_dual-oven_cook_mode_upper', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_cook_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dual cavity oven Upper oven cook mode', + 'options': list([ + 'standby', + 'bake', + 'convection_bake', + 'broil', + 'convection_broil', + 'convection_roast', + 'keep_warm', + 'air_fry', + ]), + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_cook_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'bake', + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_current_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_current_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upper oven current temperature', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_current_temperature_upper', + 'unique_id': 'said_oven_dual-oven_current_temperature_upper', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_current_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Dual cavity oven Upper oven current temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '180', + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'preheating', + 'cooking', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_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 oven state', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_state_upper', + 'unique_id': 'said_oven_dual-oven_state_upper', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Dual cavity oven Upper oven state', + 'options': list([ + 'standby', + 'preheating', + 'cooking', + ]), + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'standby', + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_target_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_target_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Upper oven target temperature', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_target_temperature_upper', + 'unique_id': 'said_oven_dual-oven_target_temperature_upper', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.dual_cavity_oven_upper_oven_target_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Dual cavity oven Upper oven target temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.dual_cavity_oven_upper_oven_target_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '200', + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_cook_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'bake', + 'convection_bake', + 'broil', + 'convection_broil', + 'convection_roast', + 'keep_warm', + 'air_fry', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.single_cavity_oven_cook_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cook mode', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_cook_mode', + 'unique_id': 'said_oven_single-oven_cook_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_cook_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Single cavity oven Cook mode', + 'options': list([ + 'standby', + 'bake', + 'convection_bake', + 'broil', + 'convection_broil', + 'convection_roast', + 'keep_warm', + 'air_fry', + ]), + }), + 'context': , + 'entity_id': 'sensor.single_cavity_oven_cook_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'bake', + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_current_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.single_cavity_oven_current_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current temperature', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_current_temperature', + 'unique_id': 'said_oven_single-oven_current_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_current_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Single cavity oven Current temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.single_cavity_oven_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '180', + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'preheating', + 'cooking', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.single_cavity_oven_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': 'State', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_state', + 'unique_id': 'said_oven_single-oven_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Single cavity oven State', + 'options': list([ + 'standby', + 'preheating', + 'cooking', + ]), + }), + 'context': , + 'entity_id': 'sensor.single_cavity_oven_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'standby', + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_target_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.single_cavity_oven_target_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Target temperature', + 'platform': 'whirlpool', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'oven_target_temperature', + 'unique_id': 'said_oven_single-oven_target_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.single_cavity_oven_target_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Single cavity oven Target temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.single_cavity_oven_target_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '200', + }) +# --- # name: test_all_entities[sensor.washer_detergent_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 92546acd77387..7fae0348d3f41 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -210,6 +210,7 @@ async def test_no_appliances_flow( mock_appliances_manager_api.return_value.aircons = [] mock_appliances_manager_api.return_value.washers = [] mock_appliances_manager_api.return_value.dryers = [] + mock_appliances_manager_api.return_value.ovens = [] result = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG_INPUT | {CONF_REGION: region[0], CONF_BRAND: brand[0]} ) diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index 463ed305d2ea4..38367f52455b3 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -83,6 +83,8 @@ async def test_setup_no_appliances( mock_appliances_manager_api.return_value.aircons = [] mock_appliances_manager_api.return_value.washers = [] mock_appliances_manager_api.return_value.dryers = [] + mock_appliances_manager_api.return_value.ovens = [] + await init_integration(hass) assert len(hass.states.async_all()) == 0 diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 85f0940fc4eca..578232f4641b0 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -6,6 +6,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from whirlpool.dryer import MachineState as DryerMachineState +from whirlpool.oven import CavityState as OvenCavityState, CookMode from whirlpool.washer import MachineState as WasherMachineState from homeassistant.components.whirlpool.sensor import SCAN_INTERVAL @@ -312,6 +313,60 @@ async def test_washer_running_states( (5, "active"), ], ), + ( + "sensor.dual_cavity_oven_upper_oven_state", + "mock_oven_dual_cavity_api", + "get_cavity_state", + [ + (OvenCavityState.Standby, "standby"), + (OvenCavityState.Preheating, "preheating"), + (OvenCavityState.Cooking, "cooking"), + (None, STATE_UNKNOWN), + ], + ), + ( + "sensor.dual_cavity_oven_upper_oven_cook_mode", + "mock_oven_dual_cavity_api", + "get_cook_mode", + [ + (CookMode.Standby, "standby"), + (CookMode.Bake, "bake"), + (CookMode.ConvectBake, "convection_bake"), + (CookMode.Broil, "broil"), + (CookMode.ConvectBroil, "convection_broil"), + (CookMode.ConvectRoast, "convection_roast"), + (CookMode.KeepWarm, "keep_warm"), + (CookMode.AirFry, "air_fry"), + (None, STATE_UNKNOWN), + ], + ), + ( + "sensor.single_cavity_oven_state", + "mock_oven_single_cavity_api", + "get_cavity_state", + [ + (OvenCavityState.Standby, "standby"), + (OvenCavityState.Preheating, "preheating"), + (OvenCavityState.Cooking, "cooking"), + (None, STATE_UNKNOWN), + ], + ), + ( + "sensor.single_cavity_oven_cook_mode", + "mock_oven_single_cavity_api", + "get_cook_mode", + [ + (CookMode.Standby, "standby"), + (CookMode.Bake, "bake"), + (CookMode.ConvectBake, "convection_bake"), + (CookMode.Broil, "broil"), + (CookMode.ConvectBroil, "convection_broil"), + (CookMode.ConvectRoast, "convection_roast"), + (CookMode.KeepWarm, "keep_warm"), + (CookMode.AirFry, "air_fry"), + (None, STATE_UNKNOWN), + ], + ), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index e7cf2c61a7660..29ececca43a06 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -5,6 +5,7 @@ from copy import deepcopy import dataclasses import io +import threading from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -46,14 +47,13 @@ entity_registry as er, service, ) -from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import ( Integration, async_get_integration, async_get_integrations, ) from homeassistant.setup import async_setup_component -from homeassistant.util.yaml.loader import parse_yaml +from homeassistant.util.yaml.loader import JSON_TYPE, parse_yaml from tests.common import ( MockEntity, @@ -849,7 +849,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: assert len(descriptions) == 1 assert DOMAIN_GROUP in descriptions - assert "description" in descriptions[DOMAIN_GROUP]["reload"] + assert "description" not in descriptions[DOMAIN_GROUP]["reload"] assert "fields" in descriptions[DOMAIN_GROUP]["reload"] # Does not have services @@ -857,26 +857,39 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: logger_config = {DOMAIN_LOGGER: {}} - async def async_get_translations( - hass: HomeAssistant, - language: str, - category: str, - integrations: Iterable[str] | None = None, - config_flow: bool | None = None, - ) -> dict[str, Any]: - """Return all backend translations.""" - translation_key_prefix = f"component.{DOMAIN_LOGGER}.services.set_default_level" + # Test legacy service with translations in services.yaml + def _load_services_file(integration: Integration) -> JSON_TYPE: return { - f"{translation_key_prefix}.name": "Translated name", - f"{translation_key_prefix}.description": "Translated description", - f"{translation_key_prefix}.fields.level.name": "Field name", - f"{translation_key_prefix}.fields.level.description": "Field description", - f"{translation_key_prefix}.fields.level.example": "Field example", + "set_default_level": { + "description": "Translated description", + "fields": { + "level": { + "description": "Field description", + "example": "Field example", + "name": "Field name", + "selector": { + "select": { + "options": [ + "debug", + "info", + "warning", + "error", + "fatal", + "critical", + ], + "translation_key": "level", + } + }, + } + }, + "name": "Translated name", + }, + "set_level": None, } with patch( - "homeassistant.helpers.service.translation.async_get_translations", - side_effect=async_get_translations, + "homeassistant.helpers.service._load_services_file", + side_effect=_load_services_file, ): await async_setup_component(hass, DOMAIN_LOGGER, logger_config) descriptions = await service.async_get_all_descriptions(hass) @@ -1003,18 +1016,11 @@ def load_yaml(fname, secrets=None): assert descriptions == { "test_domain": { "test_service": { - "description": "", "fields": { "test": { - "selector": { - "text": { - "multiline": False, - "multiple": False, - } - } + "selector": {"text": {"multiline": False, "multiple": False}} } }, - "name": "", } } } @@ -1096,7 +1102,6 @@ def load_yaml(fname, secrets=None): ) test_service_schema = { - "description": "", "fields": { "advanced_stuff": { "fields": { @@ -1155,7 +1160,6 @@ def load_yaml(fname, secrets=None): }, }, }, - "name": "", "target": { "entity": [ { @@ -1191,31 +1195,11 @@ async def wrap_get_integrations( integrations[DOMAIN_LOGGER] = ImportError("Failed to load services.yaml") return integrations - async def wrap_get_translations( - hass: HomeAssistant, - language: str, - category: str, - integrations: Iterable[str] | None = None, - config_flow: bool | None = None, - ) -> dict[str, str]: - translations = await async_get_translations( - hass, language, category, integrations, config_flow - ) - return { - key: value - for key, value in translations.items() - if not key.startswith("component.logger.services.") - } - with ( patch( "homeassistant.helpers.service.async_get_integrations", wraps=wrap_get_integrations, ), - patch( - "homeassistant.helpers.service.translation.async_get_translations", - wrap_get_translations, - ), ): descriptions = await service.async_get_all_descriptions(hass) @@ -1224,16 +1208,12 @@ async def wrap_get_translations( # Services are empty defaults if the load fails but should # not raise - assert descriptions[DOMAIN_GROUP]["remove"]["description"] + assert "description" not in descriptions[DOMAIN_GROUP]["remove"] assert descriptions[DOMAIN_GROUP]["remove"]["fields"] - assert descriptions[DOMAIN_LOGGER]["set_level"] == { - "description": "", - "fields": {}, - "name": "", - } + assert descriptions[DOMAIN_LOGGER]["set_level"] == {"fields": {}} - assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["description"] + assert "description" not in descriptions[DOMAIN_INPUT_BUTTON]["press"] assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["fields"] == {} assert "target" in descriptions[DOMAIN_INPUT_BUTTON]["press"] @@ -1288,7 +1268,7 @@ async def test_async_get_all_descriptions_dynamically_created_services( assert len(descriptions) == 1 - assert "description" in descriptions["group"]["reload"] + assert "description" not in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] shell_command_config = {DOMAIN_SHELL_COMMAND: {"test_service": "ls /bin"}} @@ -1297,9 +1277,7 @@ async def test_async_get_all_descriptions_dynamically_created_services( assert len(descriptions) == 2 assert descriptions[DOMAIN_SHELL_COMMAND]["test_service"] == { - "description": "", "fields": {}, - "name": "", "response": {"optional": True}, } @@ -1314,41 +1292,53 @@ async def test_async_get_all_descriptions_new_service_added_while_loading( assert len(descriptions) == 1 - assert "description" in descriptions["group"]["reload"] + assert "description" not in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] logger_domain = DOMAIN_LOGGER logger_config = {logger_domain: {}} - translations_called = asyncio.Event() - translations_wait = asyncio.Event() - - async def async_get_translations( - hass: HomeAssistant, - language: str, - category: str, - integrations: Iterable[str] | None = None, - config_flow: bool | None = None, - ) -> dict[str, Any]: - """Return all backend translations.""" + translations_called = threading.Event() + translations_wait = threading.Event() + + def _load_services_file(integration: Integration) -> JSON_TYPE: translations_called.set() - await translations_wait.wait() - translation_key_prefix = f"component.{logger_domain}.services.set_default_level" + translations_wait.wait() return { - f"{translation_key_prefix}.name": "Translated name", - f"{translation_key_prefix}.description": "Translated description", - f"{translation_key_prefix}.fields.level.name": "Field name", - f"{translation_key_prefix}.fields.level.description": "Field description", - f"{translation_key_prefix}.fields.level.example": "Field example", + "set_default_level": { + "description": "Translated description", + "fields": { + "level": { + "description": "Field description", + "example": "Field example", + "name": "Field name", + "selector": { + "select": { + "options": [ + "debug", + "info", + "warning", + "error", + "fatal", + "critical", + ], + "translation_key": "level", + } + }, + } + }, + "name": "Translated name", + }, + "set_level": None, } with patch( - "homeassistant.helpers.service.translation.async_get_translations", - side_effect=async_get_translations, + "homeassistant.helpers.service._load_services_file", + side_effect=_load_services_file, ): await async_setup_component(hass, logger_domain, logger_config) task = asyncio.create_task(service.async_get_all_descriptions(hass)) - await translations_called.wait() + await hass.async_add_executor_job(translations_called.wait) # Now register a new service while translations are being loaded hass.services.async_register(logger_domain, "new_service", lambda x: None, None) service.async_set_service_schema( diff --git a/tests/testing_config/blueprints/template/test_init_attribute_variables.yaml b/tests/testing_config/blueprints/template/test_init_attribute_variables.yaml new file mode 100644 index 0000000000000..7729b543eaff2 --- /dev/null +++ b/tests/testing_config/blueprints/template/test_init_attribute_variables.yaml @@ -0,0 +1,31 @@ +blueprint: + name: Switch to light + domain: template + input: + switch: + name: Switch + description: The switch which should be converted + selector: + entity: + multiple: false + filter: + - domain: switch + default: null + +variables: + switch: !input switch + on_icon: mdi:lightbulb + on_picture: "on.png" + extraa: "a" + +sensor: + variables: + off_icon: mdi:lightbulb-off + off_picture: "off.png" + extrab: "b" + name: "{{ state_attr(switch, 'friendly_name') }}" + icon: "{{ on_icon if is_state(switch, 'on') else off_icon }}" + picture: "{{ on_picture if is_state(switch, 'on') else off_picture }}" + state: "{{ is_state(switch, 'on') }}" + attributes: + extra: "{{ extraa ~ extrab }}"