diff --git a/homeassistant/components/backblaze_b2/quality_scale.yaml b/homeassistant/components/backblaze_b2/quality_scale.yaml index 1532f08d9a821b..ef20474b3130fa 100644 --- a/homeassistant/components/backblaze_b2/quality_scale.yaml +++ b/homeassistant/components/backblaze_b2/quality_scale.yaml @@ -123,8 +123,4 @@ rules: comment: | The b2sdk library does not support custom HTTP session injection. It manages HTTP connections internally through its own session management. - strict-typing: - status: exempt - comment: | - The b2sdk dependency does not include a py.typed file and is not PEP 561 compliant. - This is outside the integration's control as it's a third-party library requirement. + strict-typing: todo diff --git a/homeassistant/components/blebox/update.py b/homeassistant/components/blebox/update.py index e7e0088d32908f..389b0560c6fe22 100644 --- a/homeassistant/components/blebox/update.py +++ b/homeassistant/components/blebox/update.py @@ -22,7 +22,7 @@ from .coordinator import BleBoxCoordinator from .entity import BleBoxEntity -PARALLEL_UPDATES = 0 +PARALLEL_UPDATES = 1 SCAN_INTERVAL = timedelta(hours=1) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index dfb353b8719ee5..ade738d2f7f93a 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.18.1"], + "requirements": ["pydaikin==2.18.2"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/homeassistant/components/energieleser/__init__.py b/homeassistant/components/energieleser/__init__.py index f9167316eca074..5533dd6e91dcdd 100644 --- a/homeassistant/components/energieleser/__init__.py +++ b/homeassistant/components/energieleser/__init__.py @@ -4,8 +4,10 @@ from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .const import DOMAIN from .coordinator import EnergieleserConfigEntry, EnergieleserCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -30,4 +32,5 @@ async def async_unload_entry( hass: HomeAssistant, entry: EnergieleserConfigEntry ) -> bool: """Unload an energieleser config entry.""" + ir.async_delete_issue(hass, DOMAIN, f"pin_locked_{entry.entry_id}") return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/energieleser/coordinator.py b/homeassistant/components/energieleser/coordinator.py index e71e076425d549..6c026a69fa4770 100755 --- a/homeassistant/components/energieleser/coordinator.py +++ b/homeassistant/components/energieleser/coordinator.py @@ -9,11 +9,13 @@ EnergieleserDevice, EnergieleserError, EnergieleserUnknownDeviceError, + StromleserOneDevice, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER @@ -74,4 +76,23 @@ async def _async_update_data(self) -> EnergieleserDevice: "device_id": self.device_id, }, ) from err + if isinstance(device, StromleserOneDevice): + issue_id = f"pin_locked_{self.config_entry.entry_id}" + if device.pin_locked: + ir.async_create_issue( + self.hass, + DOMAIN, + issue_id, + is_fixable=False, + is_persistent=False, + learn_more_url="https://docs.energieleser.de/en/docs/stromleser-one/installation/preparation", + severity=ir.IssueSeverity.WARNING, + translation_key="meter_locked", + translation_placeholders={ + "device_name": self.config_entry.title, + }, + ) + else: + ir.async_delete_issue(self.hass, DOMAIN, issue_id) + return device diff --git a/homeassistant/components/energieleser/quality_scale.yaml b/homeassistant/components/energieleser/quality_scale.yaml index 7173e088629645..0a83fc235811f1 100644 --- a/homeassistant/components/energieleser/quality_scale.yaml +++ b/homeassistant/components/energieleser/quality_scale.yaml @@ -69,7 +69,7 @@ rules: exception-translations: done icon-translations: todo reconfiguration-flow: done - repair-issues: todo + repair-issues: done stale-devices: status: exempt comment: One device per config entry; the device is removed when the entry is removed. diff --git a/homeassistant/components/energieleser/strings.json b/homeassistant/components/energieleser/strings.json index 7065aec00bc331..370ec96f4b9b13 100755 --- a/homeassistant/components/energieleser/strings.json +++ b/homeassistant/components/energieleser/strings.json @@ -103,5 +103,11 @@ "unknown_device": { "message": "The device type for {device_id} is unknown or unsupported" } + }, + "issues": { + "meter_locked": { + "description": "The electricity meter connected to {device_name} is not providing high-resolution data. You need to unlock the physical meter by entering the PIN (provided by your electricity company or grid operator) directly on the meter. Once the meter is unlocked, high-resolution data will be provided and this issue will resolve itself automatically. See the linked instructions for details on how to enter the PIN.", + "title": "Meter PIN Required" + } } } diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index c8dd60ba98f257..32e591e099cd34 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -2,13 +2,11 @@ from typing import override -from homeassistant.components.device_tracker import TrackerEntity -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_GPS_ACCURACY, - ATTR_LATITUDE, - ATTR_LONGITUDE, +from homeassistant.components.device_tracker import ( + TrackerEntity, + TrackerEntityStateAttribute, ) +from homeassistant.const import ATTR_BATTERY_LEVEL, EntityStateAttribute from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -120,9 +118,11 @@ async def async_added_to_hass(self) -> None: return attr = state.attributes - self._attr_latitude = attr.get(ATTR_LATITUDE) - self._attr_longitude = attr.get(ATTR_LONGITUDE) - self._attr_location_accuracy = attr.get(ATTR_GPS_ACCURACY, 0) + self._attr_latitude = attr.get(EntityStateAttribute.LATITUDE) + self._attr_longitude = attr.get(EntityStateAttribute.LONGITUDE) + self._attr_location_accuracy = attr.get( + TrackerEntityStateAttribute.GPS_ACCURACY, 0 + ) self._attr_extra_state_attributes = { ATTR_ALTITUDE: attr.get(ATTR_ALTITUDE), ATTR_ACTIVITY: attr.get(ATTR_ACTIVITY), diff --git a/homeassistant/components/led_infrared/diagnostics.py b/homeassistant/components/led_infrared/diagnostics.py new file mode 100644 index 00000000000000..cd74ce1614eecd --- /dev/null +++ b/homeassistant/components/led_infrared/diagnostics.py @@ -0,0 +1,14 @@ +"""Diagnostics platform for the LED Infrared integration.""" + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + return dict(config_entry.data) diff --git a/homeassistant/components/led_infrared/quality_scale.yaml b/homeassistant/components/led_infrared/quality_scale.yaml index a1fe453f610202..5119557d203f32 100644 --- a/homeassistant/components/led_infrared/quality_scale.yaml +++ b/homeassistant/components/led_infrared/quality_scale.yaml @@ -65,7 +65,7 @@ rules: test-coverage: todo # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: | diff --git a/homeassistant/components/modbus_connection/manifest.json b/homeassistant/components/modbus_connection/manifest.json index a3f132e4e6d92d..7d78cd95d247fd 100644 --- a/homeassistant/components/modbus_connection/manifest.json +++ b/homeassistant/components/modbus_connection/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_polling", "loggers": ["modbus_connection", "tmodbus"], "quality_scale": "bronze", - "requirements": ["modbus-connection[tmodbus]==3.4.1"] + "requirements": ["modbus-connection[tmodbus]==3.6.0"] } diff --git a/homeassistant/components/nederlandse_spoorwegen/binary_sensor.py b/homeassistant/components/nederlandse_spoorwegen/binary_sensor.py index 7061bdc83f60d2..6d6b2a7c1e98ad 100644 --- a/homeassistant/components/nederlandse_spoorwegen/binary_sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/binary_sensor.py @@ -14,7 +14,7 @@ ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -109,6 +109,7 @@ def __init__( name=coordinator.name, manufacturer=INTEGRATION_TITLE, model=ROUTE_MODEL, + entry_type=DeviceEntryType.SERVICE, ) @property diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index c88ef824aa111c..7eef35544195ae 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -14,7 +14,7 @@ ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -202,6 +202,7 @@ def __init__( name=coordinator.name, manufacturer=INTEGRATION_TITLE, model=ROUTE_MODEL, + entry_type=DeviceEntryType.SERVICE, ) @property diff --git a/homeassistant/components/network/manifest.json b/homeassistant/components/network/manifest.json index a76da88914d6a8..7dee6332f58303 100644 --- a/homeassistant/components/network/manifest.json +++ b/homeassistant/components/network/manifest.json @@ -2,7 +2,6 @@ "domain": "network", "name": "Network Configuration", "codeowners": ["@home-assistant/core"], - "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/network", "integration_type": "system", "iot_class": "local_push", diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 2ddd05e1bbd306..06552658b0ec0c 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -56,8 +56,6 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Nobø Ecohub platform from UI configuration.""" - - # Setup connection with hub hub = config_entry.runtime_data override_type = ( @@ -66,8 +64,20 @@ async def async_setup_entry( else nobo.API.OVERRIDE_TYPE_CONSTANT ) - # Add zones as entities - async_add_entities(NoboZone(zone_id, hub, override_type) for zone_id in hub.zones) + known_zones: set[str] = set() + + @callback + def _add_zones(_hub: nobo) -> None: + """Add climate entities for zones added to the hub.""" + new_zones = [zone_id for zone_id in hub.zones if zone_id not in known_zones] + known_zones.update(new_zones) + async_add_entities( + NoboZone(zone_id, hub, override_type) for zone_id in new_zones + ) + + _add_zones(hub) + hub.register_callback(_add_zones) + config_entry.async_on_unload(lambda: hub.deregister_callback(_add_zones)) class NoboZone(NoboBaseEntity, ClimateEntity): diff --git a/homeassistant/components/nobo_hub/quality_scale.yaml b/homeassistant/components/nobo_hub/quality_scale.yaml index 28d7df4e24d186..ce62526480a978 100644 --- a/homeassistant/components/nobo_hub/quality_scale.yaml +++ b/homeassistant/components/nobo_hub/quality_scale.yaml @@ -52,13 +52,13 @@ rules: discovery: done discovery-update-info: done docs-data-update: done - docs-examples: todo + docs-examples: done docs-known-limitations: done docs-supported-devices: done docs-supported-functions: done docs-troubleshooting: done docs-use-cases: done - dynamic-devices: todo + dynamic-devices: done entity-category: todo entity-device-class: done entity-disabled-by-default: todo diff --git a/homeassistant/components/nobo_hub/select.py b/homeassistant/components/nobo_hub/select.py index 9c8313ebdfc534..40b85798d42f41 100644 --- a/homeassistant/components/nobo_hub/select.py +++ b/homeassistant/components/nobo_hub/select.py @@ -32,8 +32,6 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up any temperature sensors connected to the Nobø Ecohub.""" - - # Setup connection with hub hub = config_entry.runtime_data override_type = ( @@ -42,11 +40,22 @@ async def async_setup_entry( else nobo.API.OVERRIDE_TYPE_CONSTANT ) - entities: list[SelectEntity] = [ - NoboProfileSelector(zone_id, hub) for zone_id in hub.zones - ] - entities.append(NoboGlobalSelector(hub, override_type)) - async_add_entities(entities, True) + async_add_entities([NoboGlobalSelector(hub, override_type)], True) + + known_zones: set[str] = set() + + @callback + def _add_profiles(_hub: nobo) -> None: + """Add week-profile selectors for zones added to the hub.""" + new_zones = [zone_id for zone_id in hub.zones if zone_id not in known_zones] + known_zones.update(new_zones) + async_add_entities( + (NoboProfileSelector(zone_id, hub) for zone_id in new_zones), True + ) + + _add_profiles(hub) + hub.register_callback(_add_profiles) + config_entry.async_on_unload(lambda: hub.deregister_callback(_add_profiles)) class NoboGlobalSelector(NoboBaseEntity, SelectEntity): diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index 8ebea0b63419db..88bc76bf15069d 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -28,15 +28,26 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up any temperature sensors connected to the Nobø Ecohub.""" - - # Setup connection with hub hub = config_entry.runtime_data - async_add_entities( - NoboTemperatureSensor(component["serial"], hub) - for component in hub.components.values() - if component[ATTR_MODEL].has_temp_sensor - ) + known_components: set[str] = set() + + @callback + def _add_sensors(_hub: nobo) -> None: + """Add temperature sensors for components added to the hub.""" + new_components = [ + serial + for serial, component in hub.components.items() + if component[ATTR_MODEL].has_temp_sensor and serial not in known_components + ] + known_components.update(new_components) + async_add_entities( + NoboTemperatureSensor(serial, hub) for serial in new_components + ) + + _add_sensors(hub) + hub.register_callback(_add_sensors) + config_entry.async_on_unload(lambda: hub.deregister_callback(_add_sensors)) class NoboTemperatureSensor(NoboBaseEntity, SensorEntity): diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 4b843588c16da4..0468340432a1cb 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -14,7 +14,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz[nexity]==2.0.4"], + "requirements": ["pyoverkiz[nexity]==2.1.0"], "zeroconf": [ { "name": "gateway*", diff --git a/homeassistant/components/rympro/__init__.py b/homeassistant/components/rympro/__init__.py index 69251608d09e3e..57564aeab24bc3 100644 --- a/homeassistant/components/rympro/__init__.py +++ b/homeassistant/components/rympro/__init__.py @@ -2,7 +2,7 @@ import logging -from pyrympro import CannotConnectError, RymPro, UnauthorizedError +from pyrympro import CannotConnectError, OperationError, RymPro, UnauthorizedError from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -22,13 +22,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: RymProConfigEntry) -> bo rympro.set_token(data[CONF_TOKEN]) try: await rympro.account_info() - except CannotConnectError as error: + except (CannotConnectError, OperationError) as error: raise ConfigEntryNotReady from error except UnauthorizedError: try: token = await rympro.login(data[CONF_EMAIL], data[CONF_PASSWORD], "ha") except UnauthorizedError as error: raise ConfigEntryAuthFailed from error + except CannotConnectError as error: + raise ConfigEntryNotReady from error hass.config_entries.async_update_entry( entry, data={**data, CONF_TOKEN: token}, diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 1ba5fa21e82448..b825552e817032 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -29,7 +29,14 @@ from homeassistant.loader import async_get_integration from homeassistant.util.hass_dict import HassKey -from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN, PLATFORMS +from .const import ( + CONF_ADDITIONAL_OPTIONS, + CONF_MAX, + CONF_MIN, + CONF_STEP, + DOMAIN, + PLATFORMS, +) from .coordinator import TriggerUpdateCoordinator from .helpers import async_get_blueprints @@ -141,6 +148,14 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry, version=1, minor_version=2 ) + options = {**config_entry.options} + # The "advanced_options" section was renamed to "additional_options" + if (additional := options.pop("advanced_options", None)) is not None: + options[CONF_ADDITIONAL_OPTIONS] = additional + hass.config_entries.async_update_entry( + config_entry, options=options, version=2, minor_version=1 + ) + _LOGGER.debug( "Migration to configuration version %s.%s successful", config_entry.version, diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 0d28bc3c6aa954..934cd6a3f6bf6d 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -60,7 +60,7 @@ ) from .binary_sensor import async_create_preview_binary_sensor from .const import ( - CONF_ADVANCED_OPTIONS, + CONF_ADDITIONAL_OPTIONS, CONF_AVAILABILITY, CONF_PRESS, CONF_TURN_OFF, @@ -157,7 +157,7 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: """Generate schema.""" schema: dict[vol.Marker, Any] = {} - advanced_options: dict[vol.Marker, Any] = {} + additional_options: dict[vol.Marker, Any] = {} if flow_type == "config": schema = {vol.Required(CONF_NAME): selector.TextSelector()} @@ -240,7 +240,7 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_LATITUDE): selector.TemplateSelector(), vol.Optional(CONF_LONGITUDE): selector.TemplateSelector(), } - advanced_options |= { + additional_options |= { vol.Optional(CONF_LOCATION_ACCURACY): selector.TemplateSelector(), } @@ -445,11 +445,11 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: schema |= { vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), - vol.Optional(CONF_ADVANCED_OPTIONS): section( + vol.Optional(CONF_ADDITIONAL_OPTIONS): section( vol.Schema( { vol.Optional(CONF_AVAILABILITY): selector.TemplateSelector(), - **advanced_options, + **additional_options, } ), {"collapsed": True}, @@ -782,8 +782,7 @@ class TemplateConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): options_flow = OPTIONS_FLOW options_flow_reloads = True - MINOR_VERSION = 2 - VERSION = 1 + VERSION = 2 @callback @override @@ -901,9 +900,9 @@ def async_preview_updated( return config: dict = msg["user_input"] - advanced_options = config.pop(CONF_ADVANCED_OPTIONS, {}) + additional_options = config.pop(CONF_ADDITIONAL_OPTIONS, {}) preview_entity = CREATE_PREVIEW_ENTITY[template_type]( - hass, name, {**config, **advanced_options} + hass, name, {**config, **additional_options} ) preview_entity.hass = hass preview_entity.registry_entry = entity_registry_entry diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index cbb9c3beb27295..816b77b5284dff 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -3,7 +3,7 @@ from homeassistant.const import Platform from homeassistant.helpers.typing import ConfigType -CONF_ADVANCED_OPTIONS = "advanced_options" +CONF_ADDITIONAL_OPTIONS = "additional_options" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" CONF_ATTRIBUTES = "attributes" CONF_AVAILABILITY = "availability" diff --git a/homeassistant/components/template/helpers.py b/homeassistant/components/template/helpers.py index 959fbcb0bc371c..66ca4eec45b520 100644 --- a/homeassistant/components/template/helpers.py +++ b/homeassistant/components/template/helpers.py @@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from .const import CONF_ADVANCED_OPTIONS, CONF_DEFAULT_ENTITY_ID, DOMAIN +from .const import CONF_ADDITIONAL_OPTIONS, CONF_DEFAULT_ENTITY_ID, DOMAIN from .entity import AbstractTemplateEntity from .template_entity import TemplateEntity from .trigger_entity import TriggerEntity @@ -240,8 +240,8 @@ async def async_setup_template_entry( options = dict(config_entry.options) options.pop("template_type") - if advanced_options := options.pop(CONF_ADVANCED_OPTIONS, None): - options = {**options, **advanced_options} + if additional_options := options.pop(CONF_ADDITIONAL_OPTIONS, None): + options = {**options, **additional_options} if replace_value_template and CONF_VALUE_TEMPLATE in options: options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 6de00e8fdc7730..8c9028e03b9fa5 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -1,6 +1,6 @@ { "common": { - "advanced_options": "Advanced options", + "additional_options": "Additional options", "availability": "Availability template", "availability_description": "Defines a template to get the `available` state of the entity. If the template either fails to render or returns `True`, `\"1\"`, `\"true\"`, `\"yes\"`, `\"on\"`, `\"enable\"`, or a non-zero number, the entity will be `available`. If the template returns any other value, the entity will be `unavailable`. If not configured, the entity will always be `available`. Note that the string comparison is not case sensitive; `\"TrUe\"` and `\"yEs\"` are allowed.", "code_format": "Code format", @@ -42,14 +42,14 @@ "value_template": "Defines a template to set the state of the alarm panel. Valid output values from the template are `armed_away`, `armed_home`, `armed_night`, `armed_vacation`, `arming`, `disarmed`, `pending`, and `triggered`." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template alarm control panel" @@ -66,14 +66,14 @@ "state": "The sensor is `on` if the template evaluates as `True`, `yes`, `on`, `enable` or a positive number. Any other value will render it as `off`." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template binary sensor" @@ -90,14 +90,14 @@ "press": "Defines actions to run when button is pressed." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template button" @@ -124,14 +124,14 @@ "stop_cover": "Defines actions to run when the cover is stopped." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template cover" @@ -152,7 +152,7 @@ "name": "[%key:common::config_flow::data::name%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]", "location_accuracy": "Location accuracy" @@ -161,7 +161,7 @@ "availability": "[%key:component::template::common::availability_description%]", "location_accuracy": "Defines a template to get the accuracy of the device tracker's location in meters. Valid values are numbers greater than or equal to `0`." }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template device tracker" @@ -180,14 +180,14 @@ "event_types": "Defines a template for a list of available event types." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template event" @@ -213,14 +213,14 @@ "turn_on": "Defines actions to run when the fan is turned on. Receives variables `percentage` and/or `preset_mode`." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template fan" @@ -238,14 +238,14 @@ "verify_ssl": "Enable or disable SSL certificate verification. Disable to use an http URL, or if you have a self-signed SSL certificate and haven’t installed the CA certificate to enable verification." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template image" @@ -277,14 +277,14 @@ "turn_on": "Defines actions to run when the light is turned on." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template light" @@ -308,14 +308,14 @@ "unlock": "Defines actions to run when the lock is unlocked." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template lock" @@ -342,14 +342,14 @@ "unit_of_measurement": "Defines the unit of measurement of the number, if any." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template number" @@ -369,14 +369,14 @@ "state": "Template for the select’s current value." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template select" @@ -396,14 +396,14 @@ "unit_of_measurement": "Defines the unit of measurement for the sensor, if any. This will also display the value based on the number format setting in the user profile and influence the graphical presentation in the history visualization as a continuous value." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template sensor" @@ -423,14 +423,14 @@ "value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template switch" @@ -465,14 +465,14 @@ "update_percentage": "Defines a template to get the update completion percentage." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template update" @@ -529,14 +529,14 @@ "stop": "Defines actions to run when the vacuum is stopped." }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template vacuum" @@ -562,11 +562,11 @@ "temperature_unit": "The temperature unit" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template weather" @@ -621,14 +621,14 @@ "value_template": "[%key:component::template::config::step::alarm_control_panel::data_description::value_template%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::alarm_control_panel::title%]" @@ -644,14 +644,14 @@ "state": "[%key:component::template::config::step::binary_sensor::data_description::state%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::binary_sensor::title%]" @@ -666,14 +666,14 @@ "press": "[%key:component::template::config::step::button::data_description::press%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::button::title%]" @@ -698,14 +698,14 @@ "stop_cover": "[%key:component::template::config::step::cover::data_description::stop_cover%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::cover::title%]" @@ -724,16 +724,16 @@ "longitude": "[%key:component::template::config::step::device_tracker::data_description::longitude%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]", - "location_accuracy": "[%key:component::template::config::step::device_tracker::sections::advanced_options::data::location_accuracy%]" + "location_accuracy": "[%key:component::template::config::step::device_tracker::sections::additional_options::data::location_accuracy%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]", - "location_accuracy": "[%key:component::template::config::step::device_tracker::sections::advanced_options::data_description::location_accuracy%]" + "location_accuracy": "[%key:component::template::config::step::device_tracker::sections::additional_options::data_description::location_accuracy%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::device_tracker::title%]" @@ -751,14 +751,14 @@ "event_types": "[%key:component::template::config::step::event::data_description::event_types%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::event::title%]" @@ -783,14 +783,14 @@ "turn_on": "[%key:component::template::config::step::fan::data_description::turn_on%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::fan::title%]" @@ -807,14 +807,14 @@ "verify_ssl": "[%key:component::template::config::step::image::data_description::verify_ssl%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::image::title%]" @@ -846,14 +846,14 @@ "turn_on": "[%key:component::template::config::step::light::data_description::turn_on%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::light::title%]" @@ -876,14 +876,14 @@ "unlock": "[%key:component::template::config::step::lock::data_description::unlock%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::lock::title%]" @@ -908,14 +908,14 @@ "step": "[%key:component::template::config::step::number::data_description::step%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::number::title%]" @@ -935,14 +935,14 @@ "state": "[%key:component::template::config::step::select::data_description::state%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::select::title%]" @@ -961,14 +961,14 @@ "unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::unit_of_measurement%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::sensor::title%]" @@ -988,14 +988,14 @@ "value_template": "[%key:component::template::config::step::switch::data_description::value_template%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::switch::title%]" @@ -1030,14 +1030,14 @@ "update_percentage": "[%key:component::template::config::step::update::data_description::update_percentage%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "Template update" @@ -1071,14 +1071,14 @@ "stop": "[%key:component::template::config::step::vacuum::data_description::stop%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, "data_description": { "availability": "[%key:component::template::common::availability_description%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::vacuum::title%]" @@ -1104,11 +1104,11 @@ "temperature_unit": "[%key:component::template::config::step::weather::data_description::temperature_unit%]" }, "sections": { - "advanced_options": { + "additional_options": { "data": { "availability": "[%key:component::template::common::availability%]" }, - "name": "[%key:component::template::common::advanced_options%]" + "name": "[%key:component::template::common::additional_options%]" } }, "title": "[%key:component::template::config::step::weather::title%]" diff --git a/requirements_all.txt b/requirements_all.txt index b399346d904583..4f8d4e733c57c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1598,7 +1598,7 @@ mitsubishi-comfort==0.3.2 moat-ble==0.1.1 # homeassistant.components.modbus_connection -modbus-connection[tmodbus]==3.4.1 +modbus-connection[tmodbus]==3.6.0 # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.4.0 @@ -2109,7 +2109,7 @@ pycsspeechtts==1.0.8 pycync==0.5.0 # homeassistant.components.daikin -pydaikin==2.18.1 +pydaikin==2.18.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 @@ -2454,7 +2454,7 @@ pyotgw==2.2.3 pyotp==2.9.0 # homeassistant.components.overkiz -pyoverkiz[nexity]==2.0.4 +pyoverkiz[nexity]==2.1.0 # homeassistant.components.palazzetti pypalazzetti==0.1.20 diff --git a/tests/components/energieleser/conftest.py b/tests/components/energieleser/conftest.py index 21e8874273ad54..f26a1c4117de77 100644 --- a/tests/components/energieleser/conftest.py +++ b/tests/components/energieleser/conftest.py @@ -1,6 +1,7 @@ """Fixtures for energieleser integration tests.""" from collections.abc import Generator +from dataclasses import replace from unittest.mock import AsyncMock, patch from energieleser import ( @@ -78,6 +79,14 @@ def mock_stromleser_device() -> StromleserOneDevice: return StromleserOneDevice.from_payload(STROMLESER_API_RESPONSE) +@pytest.fixture +def mock_locked_stromleser_device( + mock_stromleser_device: StromleserOneDevice, +) -> StromleserOneDevice: + """Return a stromleser device with PIN locked.""" + return replace(mock_stromleser_device, pin_locked=True) + + @pytest.fixture def mock_gasleser_device() -> GasleserDevice: """Return a parsed gasleser device built from the API fixture.""" diff --git a/tests/components/energieleser/test_init.py b/tests/components/energieleser/test_init.py old mode 100755 new mode 100644 index b620df72992498..ce229a0dfc25f3 --- a/tests/components/energieleser/test_init.py +++ b/tests/components/energieleser/test_init.py @@ -6,18 +6,21 @@ EnergieleserConnectionError, EnergieleserError, EnergieleserUnknownDeviceError, + StromleserOneDevice, ) +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.energieleser.const import CONF_SW_VERSION, DOMAIN +from homeassistant.components.energieleser.coordinator import SCAN_INTERVAL from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from .conftest import STROMLESER_DEVICE_ID, STROMLESER_SW_VERSION -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.usefixtures("mock_energieleser_client") @@ -89,3 +92,62 @@ async def test_device_exposes_discovery_sw_version( ) assert device is not None assert device.sw_version == STROMLESER_SW_VERSION + + +async def test_meter_locked_repair_issue( + hass: HomeAssistant, + mock_energieleser_client: AsyncMock, + mock_stromleser_device: StromleserOneDevice, + mock_locked_stromleser_device: StromleserOneDevice, + mock_stromleser_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test repair issue is created when meter is locked and deleted when unlocked.""" + mock_energieleser_client.get_device.return_value = mock_locked_stromleser_device + mock_stromleser_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_stromleser_config_entry.entry_id) + await hass.async_block_till_done() + + issue_id = f"pin_locked_{mock_stromleser_config_entry.entry_id}" + issue = issue_registry.async_get_issue(DOMAIN, issue_id) + assert issue is not None + assert issue.translation_key == "meter_locked" + assert ( + issue.learn_more_url + == "https://docs.energieleser.de/en/docs/stromleser-one/installation/preparation" + ) + assert issue.translation_placeholders == { + "device_name": mock_stromleser_config_entry.title, + } + + mock_energieleser_client.get_device.return_value = mock_stromleser_device + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue(DOMAIN, issue_id) is None + + +async def test_meter_locked_repair_issue_removed_on_unload( + hass: HomeAssistant, + mock_energieleser_client: AsyncMock, + mock_locked_stromleser_device: StromleserOneDevice, + mock_stromleser_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is deleted when entry is unloaded.""" + mock_energieleser_client.get_device.return_value = mock_locked_stromleser_device + mock_stromleser_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_stromleser_config_entry.entry_id) + await hass.async_block_till_done() + + issue_id = f"pin_locked_{mock_stromleser_config_entry.entry_id}" + assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None + + assert await hass.config_entries.async_unload(mock_stromleser_config_entry.entry_id) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue(DOMAIN, issue_id) is None diff --git a/tests/components/led_infrared/snapshots/test_diagnostics.ambr b/tests/components/led_infrared/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000000..f137fce720658f --- /dev/null +++ b/tests/components/led_infrared/snapshots/test_diagnostics.ambr @@ -0,0 +1,7 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'device_type': 'generic_24_key', + 'infrared_entity_id': 'infrared.test_ir_emitter', + }) +# --- diff --git a/tests/components/led_infrared/test_diagnostics.py b/tests/components/led_infrared/test_diagnostics.py new file mode 100644 index 00000000000000..16f913428770cf --- /dev/null +++ b/tests/components/led_infrared/test_diagnostics.py @@ -0,0 +1,30 @@ +"""Test for diagnostics platform of the LED Infrared integration.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) diff --git a/tests/components/nederlandse_spoorwegen/snapshots/test_init.ambr b/tests/components/nederlandse_spoorwegen/snapshots/test_init.ambr index 96b3def82e77f4..f37f79b384bb1d 100644 --- a/tests/components/nederlandse_spoorwegen/snapshots/test_init.ambr +++ b/tests/components/nederlandse_spoorwegen/snapshots/test_init.ambr @@ -9,7 +9,7 @@ 'connections': set({ }), 'disabled_by': None, - 'entry_type': None, + 'entry_type': , 'hw_version': None, 'id': , 'identifiers': set({ @@ -38,7 +38,7 @@ 'connections': set({ }), 'disabled_by': None, - 'entry_type': None, + 'entry_type': , 'hw_version': None, 'id': , 'identifiers': set({ diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index d54a4e2b5e68d9..6309eaa183c32b 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -608,7 +608,7 @@ async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses "homeassistant.components.network.util.ifaddr.get_adapters", return_value=[], ): - assert not await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() with pytest.raises(HomeAssistantError): await network.async_get_source_ip(hass, MDNS_TARGET_IP) diff --git a/tests/components/nobo_hub/__init__.py b/tests/components/nobo_hub/__init__.py index d487b000d04d7a..48e57be118be42 100644 --- a/tests/components/nobo_hub/__init__.py +++ b/tests/components/nobo_hub/__init__.py @@ -3,6 +3,15 @@ from unittest.mock import MagicMock from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + + +def entity_unique_ids(entity_registry: er.EntityRegistry, entry_id: str) -> set[str]: + """Return the unique ids of all entities for the config entry.""" + return { + entry.unique_id + for entry in er.async_entries_for_config_entry(entity_registry, entry_id) + } async def fire_hub_update(hass: HomeAssistant, hub: MagicMock) -> None: diff --git a/tests/components/nobo_hub/test_climate.py b/tests/components/nobo_hub/test_climate.py index 2e42c59342851b..2ea1baf77b3f5c 100644 --- a/tests/components/nobo_hub/test_climate.py +++ b/tests/components/nobo_hub/test_climate.py @@ -32,11 +32,19 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from . import fire_hub_update +from . import entity_unique_ids, fire_hub_update +from .conftest import SERIAL from tests.common import MockConfigEntry, snapshot_platform CLIMATE_ENTITY = "climate.living_room_living_room" +BEDROOM_ZONE = { + "zone_id": "2", + "name": "Bedroom", + "week_profile_id": "0", + "temp_comfort_c": "22", + "temp_eco_c": "18", +} @pytest.fixture @@ -264,3 +272,20 @@ async def test_climate_action_wraps_library_error( ) assert exc_info.value.translation_domain == DOMAIN assert exc_info.value.translation_key == expected_key + + +@pytest.mark.usefixtures("init_integration") +async def test_new_zone_adds_entity( + hass: HomeAssistant, + mock_nobo_hub: MagicMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """A zone added on the hub at runtime creates a climate entity.""" + entry_id = mock_config_entry.entry_id + assert f"{SERIAL}:2" not in entity_unique_ids(entity_registry, entry_id) + + mock_nobo_hub.zones["2"] = BEDROOM_ZONE + await fire_hub_update(hass, mock_nobo_hub) + + assert f"{SERIAL}:2" in entity_unique_ids(entity_registry, entry_id) diff --git a/tests/components/nobo_hub/test_select.py b/tests/components/nobo_hub/test_select.py index 3c6871254655d2..e9a6eaa7c58fa6 100644 --- a/tests/components/nobo_hub/test_select.py +++ b/tests/components/nobo_hub/test_select.py @@ -17,7 +17,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from . import fire_hub_update +from . import entity_unique_ids, fire_hub_update +from .conftest import SERIAL from tests.common import MockConfigEntry, snapshot_platform @@ -161,3 +162,26 @@ async def test_zone_removed_marks_week_profile_unavailable( mock_nobo_hub.zones.pop("1") await fire_hub_update(hass, mock_nobo_hub) assert hass.states.get(PROFILE_ENTITY).state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("init_integration") +async def test_new_zone_adds_profile_selector( + hass: HomeAssistant, + mock_nobo_hub: MagicMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """A zone added on the hub at runtime creates a week-profile selector.""" + entry_id = mock_config_entry.entry_id + assert f"{SERIAL}:2:profile" not in entity_unique_ids(entity_registry, entry_id) + + mock_nobo_hub.zones["2"] = { + "zone_id": "2", + "name": "Bedroom", + "week_profile_id": "0", + "temp_comfort_c": "22", + "temp_eco_c": "18", + } + await fire_hub_update(hass, mock_nobo_hub) + + assert f"{SERIAL}:2:profile" in entity_unique_ids(entity_registry, entry_id) diff --git a/tests/components/nobo_hub/test_sensor.py b/tests/components/nobo_hub/test_sensor.py index 340a0611756173..8a8c3e3404ba6e 100644 --- a/tests/components/nobo_hub/test_sensor.py +++ b/tests/components/nobo_hub/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import fire_hub_update +from . import entity_unique_ids, fire_hub_update from tests.common import MockConfigEntry, snapshot_platform @@ -66,3 +66,34 @@ async def test_component_removed_marks_unavailable( mock_nobo_hub.components.pop("200000059091") await fire_hub_update(hass, mock_nobo_hub) assert hass.states.get(TEMPERATURE_ENTITY).state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("has_temp_sensor", "present"), + [(True, True), (False, False)], + ids=["temp_sensor", "no_temp_sensor"], +) +@pytest.mark.usefixtures("init_integration") +async def test_new_component_added( + hass: HomeAssistant, + mock_nobo_hub: MagicMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + has_temp_sensor: bool, + present: bool, +) -> None: + """A component yields a sensor only when it has a temperature sensor.""" + entry_id = mock_config_entry.entry_id + serial = "200000059092" + model = MagicMock() + model.name = "Panel heater" + model.has_temp_sensor = has_temp_sensor + mock_nobo_hub.components[serial] = { + "serial": serial, + "name": "Bedroom sensor", + "zone_id": "1", + "model": model, + } + await fire_hub_update(hass, mock_nobo_hub) + + assert (serial in entity_unique_ids(entity_registry, entry_id)) is present diff --git a/tests/components/rympro/test_init.py b/tests/components/rympro/test_init.py new file mode 100644 index 00000000000000..d966f9cf7e30d5 --- /dev/null +++ b/tests/components/rympro/test_init.py @@ -0,0 +1,69 @@ +"""Test the Read Your Meter Pro integration setup.""" + +from unittest.mock import patch + +from pyrympro import CannotConnectError, OperationError, UnauthorizedError +import pytest + +from homeassistant.components.rympro.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +TEST_DATA = { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + CONF_TOKEN: "test-token", + CONF_UNIQUE_ID: "test-account-number", +} + + +@pytest.fixture +def config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create a mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_DATA, + unique_id=TEST_DATA[CONF_UNIQUE_ID], + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.mark.parametrize("exception", [CannotConnectError, OperationError]) +async def test_account_info_error_retries_setup( + hass: HomeAssistant, + config_entry: MockConfigEntry, + exception: type[Exception], +) -> None: + """Test that a transient account_info error schedules a setup retry.""" + with patch( + "homeassistant.components.rympro.RymPro.account_info", + side_effect=exception, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_relogin_cannot_connect_error_retries_setup( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test that a connection error while re-authenticating retries setup.""" + with ( + patch( + "homeassistant.components.rympro.RymPro.account_info", + side_effect=UnauthorizedError, + ), + patch( + "homeassistant.components.rympro.RymPro.login", + side_effect=CannotConnectError, + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 3e1d1322e7dc67..25dac8d3b05867 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -763,6 +763,11 @@ async def _async_start(self): if self.source == ("2001:db8::", 0, 0, 1): raise OSError + # The UPnP server needs a presentation URL, which is derived from the + # instance URL. In production http is set up before ssdp; set an internal + # URL here so get_url() succeeds without relying on http being set up. + hass.config.internal_url = "http://10.10.10.10:8123" + SsdpListener.async_start = _async_start UpnpServer.async_start = _async_start await init_ssdp_component(hass) diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 934c5f9ed92d5e..bbcdf7cfab2592 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -321,7 +321,7 @@ async def test_config_flow( assert result["type"] is FlowResultType.FORM assert result["step_id"] == template_type - availability = {"advanced_options": {"availability": "{{ True }}"}} + availability = {"additional_options": {"availability": "{{ True }}"}} with patch( "homeassistant.components.template.async_setup_entry", wraps=async_setup_entry @@ -1103,7 +1103,7 @@ async def test_config_flow_preview( assert result["preview"] == "template" availability = { - "advanced_options": { + "additional_options": { "availability": "{{ is_state('binary_sensor.available', 'on') }}" } } diff --git a/tests/components/template/test_device_tracker.py b/tests/components/template/test_device_tracker.py index 59eab709bf5424..1eb3d54b490df0 100644 --- a/tests/components/template/test_device_tracker.py +++ b/tests/components/template/test_device_tracker.py @@ -155,7 +155,7 @@ async def test_setup_config_entry( options={ "name": TEST_TRACKER.object_id, **TEST_MINIMUM_REQUIREMENTS, - "advanced_options": {"location_accuracy": "{{ 10 }}"}, + "additional_options": {"location_accuracy": "{{ 10 }}"}, "template_type": device_tracker.DOMAIN, }, title="My template", diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 5c27ef80248a94..053c81280ba797 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -578,8 +578,41 @@ async def test_migration_1_1( template_entity_entry = entity_registry.async_get("sensor.my_template") assert template_entity_entry.device_id == device_entry.id - assert template_config_entry.version == 1 - assert template_config_entry.minor_version == 2 + assert template_config_entry.version == 2 + assert template_config_entry.minor_version == 1 + + +async def test_migration_1_2( + hass: HomeAssistant, +) -> None: + """Test migration from v1.2 renames the advanced_options section.""" + + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "template_type": "sensor", + "state": "{{ 'foo' }}", + "advanced_options": {"availability": "{{ True }}"}, + }, + title="My template", + version=1, + minor_version=2, + ) + template_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + assert template_config_entry.state is ConfigEntryState.LOADED + assert "advanced_options" not in template_config_entry.options + assert template_config_entry.options["additional_options"] == { + "availability": "{{ True }}" + } + + assert template_config_entry.version == 2 + assert template_config_entry.minor_version == 1 async def test_migration_from_future_version( @@ -595,7 +628,7 @@ async def test_migration_from_future_version( "state": "{{ 'foo' }}", }, title="My template", - version=2, + version=3, minor_version=1, ) config_entry.add_to_hass(hass)