diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c3a5073d03898a..e1f6061ca5652f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 + uses: github/codeql-action/init@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 + uses: github/codeql-action/analyze@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4 with: category: "/language:python" diff --git a/build.yaml b/build.yaml index 127d66145ac695..382a7498e43d59 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.1 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.1 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.1 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.1 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.1 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.2 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.2 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.2 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.2 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.2 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 2918f79ed2d2b2..5229dfddee265d 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==41.10.0", + "aioesphomeapi==41.11.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.3.0" ], diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index 54614e4899ac9d..0a76871285b8af 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -26,11 +26,14 @@ def __init__( super().__init__(coordinator) self._serial = serial self._camera_name = self.data["name"] + + connections = set() + if mac_address := self.data["mac_address"]: + connections.add((CONNECTION_NETWORK_MAC, mac_address)) + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial)}, - connections={ - (CONNECTION_NETWORK_MAC, self.data["mac_address"]), - }, + connections=connections, manufacturer=MANUFACTURER, model=self.data["device_sub_category"], name=self.data["name"], @@ -62,11 +65,14 @@ def __init__( self._serial = serial self.coordinator = coordinator self._camera_name = self.data["name"] + + connections = set() + if mac_address := self.data["mac_address"]: + connections.add((CONNECTION_NETWORK_MAC, mac_address)) + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial)}, - connections={ - (CONNECTION_NETWORK_MAC, self.data["mac_address"]), - }, + connections=connections, manufacturer=MANUFACTURER, model=self.data["device_sub_category"], name=self.data["name"], diff --git a/homeassistant/components/geniushub/entity.py b/homeassistant/components/geniushub/entity.py index 24917ab5e95ed8..e47bb59c3d39a1 100644 --- a/homeassistant/components/geniushub/entity.py +++ b/homeassistant/components/geniushub/entity.py @@ -77,10 +77,10 @@ def extra_state_attributes(self) -> dict[str, Any]: async def async_update(self) -> None: """Update an entity's state data.""" - if "_state" in self._device.data: # only via v3 API - self._last_comms = dt_util.utc_from_timestamp( - self._device.data["_state"]["lastComms"] - ) + if (state := self._device.data.get("_state")) and ( + last_comms := state.get("lastComms") + ) is not None: # only via v3 API + self._last_comms = dt_util.utc_from_timestamp(last_comms) class GeniusZone(GeniusEntity): diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index ec55a7e8c2b1d8..3bf47df83a4086 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.1.0"] + "requirements": ["pylamarzocco==2.1.1"] } diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index fa2422923bb871..415ba4d48dafab 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -100,6 +100,7 @@ SupportedModels.RGBICWW_STRIP_LIGHT.value: [Platform.LIGHT, Platform.SENSOR], SupportedModels.PLUG_MINI_EU.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.RELAY_SWITCH_2PM.value: [Platform.SWITCH, Platform.SENSOR], + SupportedModels.GARAGE_DOOR_OPENER.value: [Platform.COVER, Platform.SENSOR], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, @@ -133,6 +134,7 @@ SupportedModels.RGBICWW_STRIP_LIGHT.value: switchbot.SwitchbotRgbicLight, SupportedModels.PLUG_MINI_EU.value: switchbot.SwitchbotRelaySwitch, SupportedModels.RELAY_SWITCH_2PM.value: switchbot.SwitchbotRelaySwitch2PM, + SupportedModels.GARAGE_DOOR_OPENER.value: switchbot.SwitchbotGarageDoorOpener, } diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 247191d9c8406a..80f7978f4dc315 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -56,6 +56,7 @@ class SupportedModels(StrEnum): PLUG_MINI_EU = "plug_mini_eu" RELAY_SWITCH_2PM = "relay_switch_2pm" K11_PLUS_VACUUM = "k11+_vacuum" + GARAGE_DOOR_OPENER = "garage_door_opener" CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -91,6 +92,7 @@ class SupportedModels(StrEnum): SwitchbotModel.PLUG_MINI_EU: SupportedModels.PLUG_MINI_EU, SwitchbotModel.RELAY_SWITCH_2PM: SupportedModels.RELAY_SWITCH_2PM, SwitchbotModel.K11_VACUUM: SupportedModels.K11_PLUS_VACUUM, + SwitchbotModel.GARAGE_DOOR_OPENER: SupportedModels.GARAGE_DOOR_OPENER, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -126,6 +128,7 @@ class SupportedModels(StrEnum): SwitchbotModel.RGBICWW_FLOOR_LAMP, SwitchbotModel.PLUG_MINI_EU, SwitchbotModel.RELAY_SWITCH_2PM, + SwitchbotModel.GARAGE_DOOR_OPENER, } ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[ @@ -146,6 +149,7 @@ class SupportedModels(StrEnum): SwitchbotModel.RGBICWW_FLOOR_LAMP: switchbot.SwitchbotRgbicLight, SwitchbotModel.PLUG_MINI_EU: switchbot.SwitchbotRelaySwitch, SwitchbotModel.RELAY_SWITCH_2PM: switchbot.SwitchbotRelaySwitch2PM, + SwitchbotModel.GARAGE_DOOR_OPENER: switchbot.SwitchbotRelaySwitch, } HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = { diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9124dc7f846859..09cb13c3aea2b4 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -35,7 +35,9 @@ async def async_setup_entry( ) -> None: """Set up Switchbot curtain based on a config entry.""" coordinator = entry.runtime_data - if isinstance(coordinator.device, switchbot.SwitchbotBlindTilt): + if isinstance(coordinator.device, switchbot.SwitchbotGarageDoorOpener): + async_add_entities([SwitchbotGarageDoorOpenerEntity(coordinator)]) + elif isinstance(coordinator.device, switchbot.SwitchbotBlindTilt): async_add_entities([SwitchBotBlindTiltEntity(coordinator)]) elif isinstance(coordinator.device, switchbot.SwitchbotRollerShade): async_add_entities([SwitchBotRollerShadeEntity(coordinator)]) @@ -295,3 +297,30 @@ def _handle_coordinator_update(self) -> None: self._attr_is_closed = self.parsed_data["position"] <= 20 self.async_write_ha_state() + + +class SwitchbotGarageDoorOpenerEntity(SwitchbotEntity, CoverEntity): + """Representation of a Switchbot garage door.""" + + _device: switchbot.SwitchbotGarageDoorOpener + _attr_device_class = CoverDeviceClass.GARAGE + _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + _attr_translation_key = "garage_door" + _attr_name = None + + @property + def is_closed(self) -> bool | None: + """Return true if cover is closed, else False.""" + return not self._device.door_open() + + @exception_handler + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the garage door.""" + await self._device.open() + self.async_write_ha_state() + + @exception_handler + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the garage door.""" + await self._device.close() + self.async_write_ha_state() diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py index 7a241d6999be9f..60866154ac0e33 100644 --- a/homeassistant/components/uptimerobot/sensor.py +++ b/homeassistant/components/uptimerobot/sensor.py @@ -37,11 +37,12 @@ async def async_setup_entry( known_devices: set[int] = set() def _check_device() -> None: - current_devices = {monitor.id for monitor in coordinator.data} - new_devices = current_devices - known_devices - if new_devices: - known_devices.update(new_devices) - async_add_entities( + entities: list[UptimeRobotSensor] = [] + for monitor in coordinator.data: + if monitor.id in known_devices: + continue + known_devices.add(monitor.id) + entities.append( UptimeRobotSensor( coordinator, SensorEntityDescription( @@ -59,9 +60,9 @@ def _check_device() -> None: ), monitor=monitor, ) - for monitor in coordinator.data - if monitor.id in new_devices ) + if entities: + async_add_entities(entities) _check_device() entry.async_on_unload(coordinator.async_add_listener(_check_device)) diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py index 531131034ce0bc..41a46e9ff5cf74 100644 --- a/homeassistant/components/uptimerobot/switch.py +++ b/homeassistant/components/uptimerobot/switch.py @@ -34,11 +34,12 @@ async def async_setup_entry( known_devices: set[int] = set() def _check_device() -> None: - current_devices = {monitor.id for monitor in coordinator.data} - new_devices = current_devices - known_devices - if new_devices: - known_devices.update(new_devices) - async_add_entities( + entities: list[UptimeRobotSwitch] = [] + for monitor in coordinator.data: + if monitor.id in known_devices: + continue + known_devices.add(monitor.id) + entities.append( UptimeRobotSwitch( coordinator, SwitchEntityDescription( @@ -47,9 +48,9 @@ def _check_device() -> None: ), monitor=monitor, ) - for monitor in coordinator.data - if monitor.id in new_devices ) + if entities: + async_add_entities(entities) _check_device() entry.async_on_unload(coordinator.async_add_listener(_check_device)) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index be6efc03be9bff..944c15e7081f4e 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -376,10 +376,10 @@ async def _async_set_addon_config(self, config_updates: dict) -> None: new_addon_config = addon_config | config_updates - if not new_addon_config[CONF_ADDON_DEVICE]: - new_addon_config.pop(CONF_ADDON_DEVICE) - if not new_addon_config[CONF_ADDON_SOCKET]: - new_addon_config.pop(CONF_ADDON_SOCKET) + if new_addon_config.get(CONF_ADDON_DEVICE) is None: + new_addon_config.pop(CONF_ADDON_DEVICE, None) + if new_addon_config.get(CONF_ADDON_SOCKET) is None: + new_addon_config.pop(CONF_ADDON_SOCKET, None) if new_addon_config == addon_config: return @@ -1470,14 +1470,33 @@ async def async_step_esphome( if not is_hassio(self.hass): return self.async_abort(reason="not_hassio") - if discovery_info.zwave_home_id: - await self.async_set_unique_id(str(discovery_info.zwave_home_id)) - self._abort_if_unique_id_configured( - { - CONF_USB_PATH: None, - CONF_SOCKET_PATH: discovery_info.socket_path, - } + if ( + discovery_info.zwave_home_id + and ( + current_config_entries := self._async_current_entries( + include_ignore=False + ) ) + and (home_id := str(discovery_info.zwave_home_id)) + and ( + existing_entry := next( + ( + entry + for entry in current_config_entries + if entry.unique_id == home_id + ), + None, + ) + ) + # Only update existing entries that are configured via sockets + and existing_entry.data.get(CONF_SOCKET_PATH) + ): + await self._async_set_addon_config( + {CONF_ADDON_SOCKET: discovery_info.socket_path} + ) + # Reloading will sync add-on options to config entry data + self.hass.config_entries.async_schedule_reload(existing_entry.entry_id) + return self.async_abort(reason="already_configured") self.socket_path = discovery_info.socket_path self.context["title_placeholders"] = { diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 6c162dc08fc792..474d5e71558940 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1162,7 +1162,7 @@ class ObjectSelectorConfig(BaseSelectorConfig): fields: dict[str, ObjectSelectorField] multiple: bool label_field: str - description_field: bool + description_field: str translation_key: str diff --git a/requirements_all.txt b/requirements_all.txt index ce79798446ade0..0f7d337801383c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==1.1.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==41.10.0 +aioesphomeapi==41.11.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -2132,7 +2132,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.1.0 +pylamarzocco==2.1.1 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29b17cfc420e51..1e7a5f288c20f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==1.1.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==41.10.0 +aioesphomeapi==41.11.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -1777,7 +1777,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.1.0 +pylamarzocco==2.1.1 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 497b3b8a07d052..9fc401270fbaeb 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -1127,3 +1127,47 @@ def make_advertisement( connectable=True, tx_power=-127, ) + + +RELAY_SWITCH_1_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Relay Switch 1", + manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b";\x00\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Relay Switch 1", + manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"=\x00\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Relay Switch 1"), + time=0, + connectable=True, + tx_power=-127, +) + + +GARAGE_DOOR_OPENER_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Garage Door Opener", + manufacturer_data={2409: b"$X|\x05BN\x0f\x00\x00\x03\x00\x00\x00\x00\x00\x00"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b">\x00\x00\x00", + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Garage Door Opener", + manufacturer_data={2409: b"$X|\x05BN\x0f\x00\x00\x03\x00\x00\x00\x00\x00\x00"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b">\x00\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Garage Door Opener"), + time=0, + connectable=True, + tx_power=-127, +) diff --git a/tests/components/switchbot/test_cover.py b/tests/components/switchbot/test_cover.py index 9430a45d106dc3..670e855d8f884f 100644 --- a/tests/components/switchbot/test_cover.py +++ b/tests/components/switchbot/test_cover.py @@ -30,6 +30,7 @@ from homeassistant.exceptions import HomeAssistantError from . import ( + GARAGE_DOOR_OPENER_SERVICE_INFO, ROLLER_SHADE_SERVICE_INFO, WOBLINDTILT_SERVICE_INFO, WOCURTAIN3_SERVICE_INFO, @@ -648,3 +649,41 @@ async def test_exception_handling_cover_service( {**service_data, ATTR_ENTITY_ID: entity_id}, blocking=True, ) + + +@pytest.mark.parametrize( + ("service", "mock_method"), + [ + (SERVICE_OPEN_COVER, "open"), + (SERVICE_CLOSE_COVER, "close"), + ], +) +async def test_garage_door_opener_controlling( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], + service: str, + mock_method: str, +) -> None: + """Test Garage Door Opener controlling.""" + inject_bluetooth_service_info(hass, GARAGE_DOOR_OPENER_SERVICE_INFO) + + entry = mock_entry_encrypted_factory(sensor_type="garage_door_opener") + entry.add_to_hass(hass) + entity_id = "cover.test_name" + + mocked_instance = AsyncMock(return_value=True) + with patch.multiple( + "homeassistant.components.switchbot.cover.switchbot.SwitchbotGarageDoorOpener", + update=AsyncMock(), + **{mock_method: mocked_instance}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mocked_instance.assert_awaited_once() diff --git a/tests/components/switchbot/test_switch.py b/tests/components/switchbot/test_switch.py index edab2fdaddcf6e..3754dbf8170e0b 100644 --- a/tests/components/switchbot/test_switch.py +++ b/tests/components/switchbot/test_switch.py @@ -19,8 +19,10 @@ from . import ( PLUG_MINI_EU_SERVICE_INFO, + RELAY_SWITCH_1_SERVICE_INFO, RELAY_SWITCH_2PM_SERVICE_INFO, WOHAND_SERVICE_INFO, + WORELAY_SWITCH_1PM_SERVICE_INFO, ) from tests.common import MockConfigEntry, mock_restore_cache @@ -114,6 +116,8 @@ async def test_exception_handling_switch( ("sensor_type", "service_info"), [ ("plug_mini_eu", PLUG_MINI_EU_SERVICE_INFO), + ("relay_switch_1", RELAY_SWITCH_1_SERVICE_INFO), + ("relay_switch_1pm", WORELAY_SWITCH_1PM_SERVICE_INFO), ], ) @pytest.mark.parametrize( @@ -207,11 +211,37 @@ async def test_relay_switch_2pm_control( @pytest.mark.parametrize( - ("exception", "error_message"), + ("sensor_type", "service_info", "entity_id", "mock_class"), [ ( - SwitchbotOperationError("Operation failed"), - "An error occurred while performing the action: Operation failed", + "relay_switch_1", + RELAY_SWITCH_1_SERVICE_INFO, + "switch.test_name", + "SwitchbotRelaySwitch", + ), + ( + "relay_switch_1pm", + WORELAY_SWITCH_1PM_SERVICE_INFO, + "switch.test_name", + "SwitchbotRelaySwitch", + ), + ( + "plug_mini_eu", + PLUG_MINI_EU_SERVICE_INFO, + "switch.test_name", + "SwitchbotRelaySwitch", + ), + ( + "relay_switch_2pm", + RELAY_SWITCH_2PM_SERVICE_INFO, + "switch.test_name_channel_1", + "SwitchbotRelaySwitch2PM", + ), + ( + "relay_switch_2pm", + RELAY_SWITCH_2PM_SERVICE_INFO, + "switch.test_name_channel_2", + "SwitchbotRelaySwitch2PM", ), ], ) @@ -223,29 +253,34 @@ async def test_relay_switch_2pm_control( ], ) @pytest.mark.parametrize( - "entry_id", + ("exception", "error_message"), [ - "switch.test_name_channel_1", - "switch.test_name_channel_2", + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), ], ) -async def test_relay_switch_2pm_exception( +async def test_relay_switch_control_with_exception( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], - exception: Exception, - error_message: str, + sensor_type: str, + service_info: BluetoothServiceInfoBleak, + entity_id: str, + mock_class: str, service: str, mock_method: str, - entry_id: str, + exception: Exception, + error_message: str, ) -> None: - """Test Relay Switch 2PM exception handling.""" - inject_bluetooth_service_info(hass, RELAY_SWITCH_2PM_SERVICE_INFO) + """Test Relay Switch control with exception.""" + inject_bluetooth_service_info(hass, service_info) - entry = mock_entry_encrypted_factory(sensor_type="relay_switch_2pm") + entry = mock_entry_encrypted_factory(sensor_type=sensor_type) entry.add_to_hass(hass) with patch.multiple( - "homeassistant.components.switchbot.switch.switchbot.SwitchbotRelaySwitch2PM", + f"homeassistant.components.switchbot.switch.switchbot.{mock_class}", update=AsyncMock(return_value=None), **{mock_method: AsyncMock(side_effect=exception)}, ): @@ -256,6 +291,6 @@ async def test_relay_switch_2pm_exception( await hass.services.async_call( SWITCH_DOMAIN, service, - {ATTR_ENTITY_ID: entry_id}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 42bad7e0f55e43..1345247b092461 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1290,6 +1290,49 @@ async def test_esphome_discovery( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") +async def test_esphome_discovery_already_configured( + hass: HomeAssistant, + set_addon_options: AsyncMock, + addon_options: dict[str, Any], +) -> None: + """Test ESPHome discovery success path.""" + addon_options[CONF_ADDON_SOCKET] = "esphome://existing-device:6053" + addon_options["another_key"] = "should_not_be_touched" + + entry = MockConfigEntry( + entry_id="mock-entry-id", + domain=DOMAIN, + data={CONF_SOCKET_PATH: "esphome://existing-device:6053"}, + title=TITLE, + unique_id="1234", + ) + entry.add_to_hass(hass) + + with patch.object(hass.config_entries, "async_schedule_reload") as mock_reload: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ESPHOME}, + data=ESPHOME_DISCOVERY_INFO, + ) + + mock_reload.assert_called_once_with(entry.entry_id) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Addon got updated + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "socket": "esphome://192.168.1.100:6053", + "another_key": "should_not_be_touched", + } + ), + ) + + @pytest.mark.usefixtures("supervisor", "addon_installed") async def test_discovery_addon_not_running( hass: HomeAssistant,