diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a89f7941ea7d7..0ab94aa4e8993 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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: category: "/language:python" diff --git a/homeassistant/components/miele/button.py b/homeassistant/components/miele/button.py index 4086c0027434c..dda2dc7ef3032 100644 --- a/homeassistant/components/miele/button.py +++ b/homeassistant/components/miele/button.py @@ -6,7 +6,7 @@ import logging from typing import Final -import aiohttp +from aiohttp import ClientResponseError from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant @@ -153,11 +153,12 @@ async def async_press(self) -> None: self._device_id, {PROCESS_ACTION: self.entity_description.press_data}, ) - except aiohttp.ClientResponseError as ex: + except ClientResponseError as err: + _LOGGER.debug("Error setting button state for %s: %s", self.entity_id, err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", translation_placeholders={ "entity": self.entity_id, }, - ) from ex + ) from err diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 07637c817b102..d682a7111b1d9 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -7,7 +7,7 @@ import logging from typing import Any, Final, cast -import aiohttp +from aiohttp import ClientResponseError from pymiele import MieleDevice, MieleTemperature from homeassistant.components.climate import ( @@ -250,7 +250,8 @@ async def async_set_temperature(self, **kwargs: Any) -> None: cast(float, kwargs.get(ATTR_TEMPERATURE)), self.entity_description.zone, ) - except aiohttp.ClientError as err: + except ClientResponseError as err: + _LOGGER.debug("Error setting climate state for %s: %s", self.entity_id, err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index b3eb1185bd1af..5a94f58477c3e 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -73,7 +73,7 @@ async def _async_update_data(self) -> MieleCoordinatorData: _LOGGER.debug( "Error fetching actions for device %s: Status: %s, Message: %s", device_id, - err.status, + str(err.status), err.message, ) actions_json = {} diff --git a/homeassistant/components/miele/fan.py b/homeassistant/components/miele/fan.py index 5faaa46b33c75..9561c0dc17ae2 100644 --- a/homeassistant/components/miele/fan.py +++ b/homeassistant/components/miele/fan.py @@ -142,14 +142,15 @@ async def async_set_percentage(self, percentage: int) -> None: await self.api.send_action( self._device_id, {VENTILATION_STEP: ventilation_step} ) - except ClientResponseError as ex: + except ClientResponseError as err: + _LOGGER.debug("Error setting fan state for %s: %s", self.entity_id, err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", translation_placeholders={ "entity": self.entity_id, }, - ) from ex + ) from err self.device.state_ventilation_step = ventilation_step self.async_write_ha_state() @@ -171,6 +172,7 @@ async def async_turn_on( translation_key="set_state_error", translation_placeholders={ "entity": self.entity_id, + "err_status": str(ex.status), }, ) from ex @@ -188,6 +190,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: translation_key="set_state_error", translation_placeholders={ "entity": self.entity_id, + "err_status": str(ex.status), }, ) from ex diff --git a/homeassistant/components/miele/light.py b/homeassistant/components/miele/light.py index e918b93b12a8f..bcdaf920ef9a8 100644 --- a/homeassistant/components/miele/light.py +++ b/homeassistant/components/miele/light.py @@ -7,7 +7,7 @@ import logging from typing import Any, Final -import aiohttp +from aiohttp import ClientResponseError from homeassistant.components.light import ( ColorMode, @@ -131,7 +131,8 @@ async def async_turn_light(self, mode: int) -> None: await self.api.send_action( self._device_id, {self.entity_description.light_type: mode} ) - except aiohttp.ClientError as err: + except ClientResponseError as err: + _LOGGER.debug("Error setting light state for %s: %s", self.entity_id, err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", diff --git a/homeassistant/components/miele/services.py b/homeassistant/components/miele/services.py index da8ee861f46d9..47d1f035563a0 100644 --- a/homeassistant/components/miele/services.py +++ b/homeassistant/components/miele/services.py @@ -4,7 +4,7 @@ import logging from typing import cast -import aiohttp +from aiohttp import ClientResponseError import voluptuous as vol from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE @@ -107,7 +107,7 @@ async def set_program(call: ServiceCall) -> None: data = {"programId": call.data[ATTR_PROGRAM_ID]} try: await api.set_program(serial_number, data) - except aiohttp.ClientResponseError as ex: + except ClientResponseError as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_program_error", @@ -137,7 +137,7 @@ async def set_program_oven(call: ServiceCall) -> None: data["temperature"] = call.data[ATTR_TEMPERATURE] try: await api.set_program(serial_number, data) - except aiohttp.ClientResponseError as ex: + except ClientResponseError as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_program_oven_error", @@ -157,7 +157,7 @@ async def get_programs(call: ServiceCall) -> ServiceResponse: try: programs = await api.get_programs(serial_number) - except aiohttp.ClientResponseError as ex: + except ClientResponseError as ex: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="get_programs_error", diff --git a/homeassistant/components/miele/switch.py b/homeassistant/components/miele/switch.py index af46ef2c917e6..277cf56e639ee 100644 --- a/homeassistant/components/miele/switch.py +++ b/homeassistant/components/miele/switch.py @@ -7,7 +7,7 @@ import logging from typing import Any, Final, cast -import aiohttp +from aiohttp import ClientResponseError from pymiele import MieleDevice from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -165,7 +165,8 @@ async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None: """Set switch to mode.""" try: await self.api.send_action(self._device_id, mode) - except aiohttp.ClientError as err: + except ClientResponseError as err: + _LOGGER.debug("Error setting switch state for %s: %s", self.entity_id, err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", @@ -197,7 +198,8 @@ async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None: """Set switch to mode.""" try: await self.api.send_action(self._device_id, mode) - except aiohttp.ClientError as err: + except ClientResponseError as err: + _LOGGER.debug("Error setting switch state for %s: %s", self.entity_id, err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", diff --git a/homeassistant/components/miele/vacuum.py b/homeassistant/components/miele/vacuum.py index 8ca2713f59fdc..fd52ddf742552 100644 --- a/homeassistant/components/miele/vacuum.py +++ b/homeassistant/components/miele/vacuum.py @@ -189,14 +189,15 @@ async def send(self, device_id: str, action: dict[str, Any]) -> None: """Send action to the device.""" try: await self.api.send_action(device_id, action) - except ClientResponseError as ex: + except ClientResponseError as err: + _LOGGER.debug("Error setting vacuum state for %s: %s", self.entity_id, err) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_state_error", translation_placeholders={ "entity": self.entity_id, }, - ) from ex + ) from err async def async_clean_spot(self, **kwargs: Any) -> None: """Clean spot.""" diff --git a/homeassistant/components/open_router/manifest.json b/homeassistant/components/open_router/manifest.json index 1c7b8b3f36bc8..53b94655124be 100644 --- a/homeassistant/components/open_router/manifest.json +++ b/homeassistant/components/open_router/manifest.json @@ -9,5 +9,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["openai==2.2.0", "python-open-router==0.3.2"] + "requirements": ["openai==2.2.0", "python-open-router==0.3.3"] } diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index 5f532378beecd..9f96ca37a81fc 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -5,6 +5,7 @@ from typing import Any from aiosenz import MODE_AUTO, Thermostat +from httpx import RequestError from homeassistant.components.climate import ( ClimateEntity, @@ -14,6 +15,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -81,7 +83,7 @@ def target_temperature(self) -> float: @property def available(self) -> bool: """Return True if the thermostat is available.""" - return self._thermostat.online + return super().available and self._thermostat.online @property def hvac_mode(self) -> HVACMode: @@ -97,14 +99,32 @@ def hvac_action(self) -> HVACAction: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - if hvac_mode == HVACMode.AUTO: - await self._thermostat.auto() - else: - await self._thermostat.manual() + try: + if hvac_mode == HVACMode.AUTO: + await self._thermostat.auto() + else: + await self._thermostat.manual() + except RequestError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_attribute_error", + translation_placeholders={ + "attribute": "hvac mode", + }, + ) from err await self.coordinator.async_request_refresh() async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp: float = kwargs[ATTR_TEMPERATURE] - await self._thermostat.manual(temp) + try: + await self._thermostat.manual(temp) + except RequestError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_attribute_error", + translation_placeholders={ + "attribute": "target temperature", + }, + ) from err await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/senz/strings.json b/homeassistant/components/senz/strings.json index cc259261ba315..320647d3f5b57 100644 --- a/homeassistant/components/senz/strings.json +++ b/homeassistant/components/senz/strings.json @@ -42,6 +42,9 @@ }, "oauth2_implementation_unavailable": { "message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]" + }, + "set_attribute_error": { + "message": "Failed to set {attribute} on the device." } } } diff --git a/homeassistant/components/shelly/icons.json b/homeassistant/components/shelly/icons.json index 1e4e40287f492..cabef1d5c0a1e 100644 --- a/homeassistant/components/shelly/icons.json +++ b/homeassistant/components/shelly/icons.json @@ -77,16 +77,26 @@ "on": "mdi:home-export-outline" } }, - "cury_boost": { + "left_slot": { + "default": "mdi:scent", + "state": { + "off": "mdi:scent-off", + "on": "mdi:scent" + } + }, + "left_slot_boost": { "default": "mdi:rocket-launch" }, - "cury_slot": { + "right_slot": { "default": "mdi:scent", "state": { "off": "mdi:scent-off", "on": "mdi:scent" } }, + "right_slot_boost": { + "default": "mdi:rocket-launch" + }, "valve_switch": { "default": "mdi:valve", "state": { diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 56e389f5023b7..59fa41bbb2ec5 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -463,6 +463,41 @@ "name": "Water temperature" } }, + "switch": { + "charging": { + "name": "Charging" + }, + "child_lock": { + "name": "Child lock" + }, + "frost_protection": { + "name": "[%key:component::shelly::entity::climate::thermostat::state_attributes::preset_mode::state::frost_protection%]" + }, + "left_slot": { + "name": "Left slot" + }, + "left_slot_boost": { + "name": "Left slot boost" + }, + "motion_detection": { + "name": "Motion detection" + }, + "right_slot": { + "name": "Right slot" + }, + "right_slot_boost": { + "name": "Right slot boost" + }, + "thermostat_enabled": { + "name": "Thermostat enabled" + }, + "valve_opened": { + "name": "Valve opened" + }, + "zone_with_number": { + "name": "Zone {zone_number}" + } + }, "update": { "beta_firmware": { "name": "Beta firmware" diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 10fc7848ea897..401a178538346 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -67,7 +67,7 @@ class BlockSwitchDescription(BlockEntityDescription, SwitchEntityDescription): BLOCK_SLEEPING_MOTION_SWITCH = { ("sensor", "motionActive"): BlockSwitchDescription( key="sensor|motionActive", - name="Motion detection", + translation_key="motion_detection", entity_category=EntityCategory.CONFIG, ) } @@ -99,7 +99,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_generic": RpcSwitchDescription( key="boolean", sub_key="value", - removal_condition=lambda config, _status, key: not is_view_for_platform( + removal_condition=lambda config, _, key: not is_view_for_platform( config, key, SWITCH_PLATFORM ), is_on=lambda status: bool(status["value"]), @@ -111,6 +111,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_anti_freeze": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="frost_protection", entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -122,6 +123,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_child_lock": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="child_lock", is_on=lambda status: bool(status["value"]), method_on="boolean_set", method_off="boolean_set", @@ -132,6 +134,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_enable": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="thermostat_enabled", entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -143,7 +146,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_start_charging": RpcSwitchDescription( key="boolean", sub_key="value", - name="Charging", + translation_key="charging", is_on=lambda status: bool(status["value"]), method_on="boolean_set", method_off="boolean_set", @@ -154,6 +157,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_state": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="valve_opened", entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -165,6 +169,8 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_zone0": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="zone_with_number", + translation_placeholders={"zone_number": "1"}, entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -176,6 +182,8 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_zone1": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="zone_with_number", + translation_placeholders={"zone_number": "2"}, entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -187,6 +195,8 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_zone2": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="zone_with_number", + translation_placeholders={"zone_number": "3"}, entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -198,6 +208,8 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_zone3": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="zone_with_number", + translation_placeholders={"zone_number": "4"}, entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -209,6 +221,8 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_zone4": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="zone_with_number", + translation_placeholders={"zone_number": "5"}, entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -220,6 +234,8 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "boolean_zone5": RpcSwitchDescription( key="boolean", sub_key="value", + translation_key="zone_with_number", + translation_placeholders={"zone_number": "6"}, entity_registry_enabled_default=False, is_on=lambda status: bool(status["value"]), method_on="boolean_set", @@ -241,8 +257,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "cury_left": RpcSwitchDescription( key="cury", sub_key="slots", - name="Left slot", - translation_key="cury_slot", + translation_key="left_slot", is_on=lambda status: bool(status["slots"]["left"]["on"]), method_on="cury_set", method_off="cury_set", @@ -254,8 +269,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "cury_left_boost": RpcSwitchDescription( key="cury", sub_key="slots", - name="Left slot boost", - translation_key="cury_boost", + translation_key="left_slot_boost", is_on=lambda status: status["slots"]["left"]["boost"] is not None, method_on="cury_boost", method_off="cury_stop_boost", @@ -267,8 +281,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "cury_right": RpcSwitchDescription( key="cury", sub_key="slots", - name="Right slot", - translation_key="cury_slot", + translation_key="right_slot", is_on=lambda status: bool(status["slots"]["right"]["on"]), method_on="cury_set", method_off="cury_set", @@ -280,8 +293,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription): "cury_right_boost": RpcSwitchDescription( key="cury", sub_key="slots", - name="Right slot boost", - translation_key="cury_boost", + translation_key="right_slot_boost", is_on=lambda status: status["slots"]["right"]["boost"] is not None, method_on="cury_boost", method_off="cury_stop_boost", @@ -399,7 +411,6 @@ class BlockSleepingMotionSwitch( """Entity that controls Motion Sensor on Block based Shelly devices.""" entity_description: BlockSwitchDescription - _attr_translation_key = "motion_switch" def __init__( self, @@ -413,6 +424,9 @@ def __init__( super().__init__(coordinator, block, attribute, description, entry) self.last_state: State | None = None + if hasattr(self, "_attr_name"): + delattr(self, "_attr_name") + @property def is_on(self) -> bool | None: """If motion is active.""" @@ -488,6 +502,23 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity): entity_description: RpcSwitchDescription + def __init__( + self, + coordinator: ShellyRpcCoordinator, + key: str, + attribute: str, + description: RpcSwitchDescription, + ) -> None: + """Initialize select.""" + super().__init__(coordinator, key, attribute, description) + + if ( + hasattr(self, "_attr_name") + and description.role != ROLE_GENERIC + and description.key not in ("switch", "script") + ): + delattr(self, "_attr_name") + @property def is_on(self) -> bool: """If switch is on.""" @@ -524,7 +555,7 @@ def __init__( coordinator: ShellyRpcCoordinator, key: str, attribute: str, - description: RpcEntityDescription, + description: RpcSwitchDescription, ) -> None: """Initialize the switch.""" super().__init__(coordinator, key, attribute, description) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index fc64dbb7cb26a..9f3a31bcfbae9 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -281,10 +281,6 @@ def __init__( ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - await super().async_added_to_hass() - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVACMode.OFF}] diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 40d28e46cf2e9..acb8afafa8aec 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.78"], + "requirements": ["zha==0.0.79"], "usb": [ { "description": "*2652*", diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9e7323d390a02..4265e776c392e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1471,16 +1471,18 @@ async def async_init( if not self._pending_import_flows[handler]: del self._pending_import_flows[handler] - if ( - result["type"] != data_entry_flow.FlowResultType.ABORT - and source in DISCOVERY_SOURCES - ): + # Flows can abort or create an entry in their initial step, we do not want to + # fire a discovery event if no flow is actually in progress + flow_completed = result["type"] in { + data_entry_flow.FlowResultType.ABORT, + data_entry_flow.FlowResultType.CREATE_ENTRY, + } + + if not flow_completed and source in DISCOVERY_SOURCES: # Fire discovery event await self._discovery_event_debouncer.async_call() - if result["type"] != data_entry_flow.FlowResultType.ABORT and source in ( - DISCOVERY_SOURCES | {SOURCE_REAUTH} - ): + if not flow_completed and source in DISCOVERY_SOURCES | {SOURCE_REAUTH}: # Notify listeners that a flow is created for subscription in self._flow_subscriptions: subscription("added", flow.flow_id) diff --git a/requirements_all.txt b/requirements_all.txt index a9a7cb1232bce..d5e30601c8b9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2506,7 +2506,7 @@ python-mpd2==3.1.1 python-mystrom==2.5.0 # homeassistant.components.open_router -python-open-router==0.3.2 +python-open-router==0.3.3 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 @@ -3225,7 +3225,7 @@ zeroconf==0.148.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.78 +zha==0.0.79 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29c8152383419..01d5104708851 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2075,7 +2075,7 @@ python-mpd2==3.1.1 python-mystrom==2.5.0 # homeassistant.components.open_router -python-open-router==0.3.2 +python-open-router==0.3.3 # homeassistant.components.swiss_public_transport python-opendata-transport==0.5.0 @@ -2668,7 +2668,7 @@ zeroconf==0.148.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.78 +zha==0.0.79 # homeassistant.components.zwave_js zwave-js-server-python==0.67.1 diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 17703c0958b08..f43167f11218b 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -3,7 +3,7 @@ from collections.abc import Generator from http import HTTPStatus from typing import Any -from unittest.mock import ANY, AsyncMock, patch +from unittest.mock import ANY, AsyncMock, Mock, patch from aiohttp.test_utils import TestClient from freezegun.api import FrozenDateTimeFactory @@ -1002,6 +1002,36 @@ async def async_step_reconfigure( } +async def test_get_progress_subscribe_create_entry(hass: HomeAssistant) -> None: + """Test flows creating entry immediately don't trigger subscription notification.""" + assert await async_setup_component(hass, "config", {}) + mock_platform(hass, "test.config_flow", None) + + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) + + class TestFlow(core_ce.ConfigFlow): + VERSION = 1 + + async def async_step_import( + self, user_input: dict[str, Any] + ) -> ConfigFlowResult: + """Handle import - creates entry immediately.""" + return self.async_create_entry(title="Test", data={}) + + subscription_mock = Mock() + hass.config_entries.flow.async_subscribe_flow(subscription_mock) + + with mock_config_flow("test", TestFlow): + result = await hass.config_entries.flow.async_init( + "test", context={"source": core_ce.SOURCE_IMPORT}, data={} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert len(subscription_mock.mock_calls) == 0 + + async def test_get_progress_subscribe_in_progress( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index 6cbae344a41c5..ae032abf56d14 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from aiohttp import ClientError +from aiohttp import ClientResponseError import pytest from syrupy.assertion import SnapshotAssertion @@ -107,7 +107,9 @@ async def test_api_failure( setup_platform: MockConfigEntry, ) -> None: """Test handling of exception from API.""" - mock_miele_client.set_target_temperature.side_effect = ClientError + mock_miele_client.set_target_temperature.side_effect = ClientResponseError( + "test", "Test" + ) with pytest.raises( HomeAssistantError, match=f"Failed to set state for {ENTITY_ID}" diff --git a/tests/components/miele/test_light.py b/tests/components/miele/test_light.py index 85f1fcd8d0411..52c1667c7988b 100644 --- a/tests/components/miele/test_light.py +++ b/tests/components/miele/test_light.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from aiohttp import ClientError +from aiohttp import ClientResponseError import pytest from syrupy.assertion import SnapshotAssertion @@ -84,7 +84,7 @@ async def test_api_failure( service: str, ) -> None: """Test handling of exception from API.""" - mock_miele_client.send_action.side_effect = ClientError + mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test") with pytest.raises( HomeAssistantError, match=f"Failed to set state for {ENTITY_ID}" diff --git a/tests/components/miele/test_switch.py b/tests/components/miele/test_switch.py index 7115432cfba48..a6da3332441e5 100644 --- a/tests/components/miele/test_switch.py +++ b/tests/components/miele/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from aiohttp import ClientError +from aiohttp import ClientResponseError import pytest from syrupy.assertion import SnapshotAssertion @@ -99,7 +99,7 @@ async def test_api_failure( entity: str, ) -> None: """Test handling of exception from API.""" - mock_miele_client.send_action.side_effect = ClientError + mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test") with pytest.raises(HomeAssistantError, match=f"Failed to set state for {entity}"): await hass.services.async_call( diff --git a/tests/components/senz/snapshots/test_climate.ambr b/tests/components/senz/snapshots/test_climate.ambr new file mode 100644 index 0000000000000..ad217da1bfa42 --- /dev/null +++ b/tests/components/senz/snapshots/test_climate.ambr @@ -0,0 +1,131 @@ +# serializer version: 1 +# name: test_climate_snapshot[climate.test_room_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.test_room_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'senz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '1001', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_snapshot[climate.test_room_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 18.4, + 'friendly_name': 'Test room 1', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 5, + 'supported_features': , + 'temperature': 19.0, + }), + 'context': , + 'entity_id': 'climate.test_room_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_climate_snapshot[climate.test_room_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.test_room_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'senz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '1002', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_snapshot[climate.test_room_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 9.3, + 'friendly_name': 'Test room 2', + 'hvac_action': , + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 5, + 'supported_features': , + 'temperature': 6.0, + }), + 'context': , + 'entity_id': 'climate.test_room_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- diff --git a/tests/components/senz/test_climate.py b/tests/components/senz/test_climate.py new file mode 100644 index 0000000000000..cc104b744db3a --- /dev/null +++ b/tests/components/senz/test_climate.py @@ -0,0 +1,153 @@ +"""Test Senz climate platform.""" + +from unittest.mock import MagicMock, patch + +from httpx import RequestError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + DOMAIN as CLIMATE_DOMAIN, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +TEST_DOMAIN = CLIMATE_DOMAIN +TEST_ENTITY_ID = "climate.test_room_1" +SERVICE_SET_TEMPERATURE = "set_temperature" +SERVICE_SET_HVAC_MODE = "set_hvac_mode" + + +async def test_climate_snapshot( + hass: HomeAssistant, + mock_senz_client: MagicMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test climate setup for cloud connection.""" + with patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +async def test_set_target( + hass: HomeAssistant, + mock_senz_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting of target temperature.""" + + with ( + patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]), + patch( + "homeassistant.components.senz.Thermostat.manual", return_value=None + ) as mock_manual, + ): + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + TEST_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_TEMPERATURE: 17}, + blocking=True, + ) + mock_manual.assert_called_once_with(17.0) + + +async def test_set_target_fail( + hass: HomeAssistant, + mock_senz_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that failed set_temperature is handled.""" + + with ( + patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]), + patch( + "homeassistant.components.senz.Thermostat.manual", + side_effect=RequestError("API error"), + ) as mock_manual, + ): + await setup_integration(hass, mock_config_entry) + with pytest.raises( + HomeAssistantError, match="Failed to set target temperature on the device" + ): + await hass.services.async_call( + TEST_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_TEMPERATURE: 17}, + blocking=True, + ) + mock_manual.assert_called_once() + + +@pytest.mark.parametrize( + ("mode", "manual_count", "auto_count"), + [(HVACMode.HEAT, 1, 0), (HVACMode.AUTO, 0, 1)], +) +async def test_set_hvac_mode( + hass: HomeAssistant, + mock_senz_client: MagicMock, + mock_config_entry: MockConfigEntry, + mode: str, + manual_count: int, + auto_count: int, +) -> None: + """Test setting of hvac mode.""" + + with ( + patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]), + patch( + "homeassistant.components.senz.Thermostat.manual", return_value=None + ) as mock_manual, + patch( + "homeassistant.components.senz.Thermostat.auto", return_value=None + ) as mock_auto, + ): + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + TEST_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HVAC_MODE: mode}, + blocking=True, + ) + assert mock_manual.call_count == manual_count + assert mock_auto.call_count == auto_count + + +async def test_set_hvac_mode_fail( + hass: HomeAssistant, + mock_senz_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that failed set_hvac_mode is handled.""" + + with ( + patch("homeassistant.components.senz.PLATFORMS", [Platform.CLIMATE]), + patch( + "homeassistant.components.senz.Thermostat.manual", + side_effect=RequestError("API error"), + ) as mock_manual, + ): + await setup_integration(hass, mock_config_entry) + with pytest.raises( + HomeAssistantError, match="Failed to set hvac mode on the device" + ): + await hass.services.async_call( + TEST_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_manual.assert_called_once() diff --git a/tests/components/shelly/snapshots/test_devices.ambr b/tests/components/shelly/snapshots/test_devices.ambr index de93c1eea7745..d24a3d0cd1c01 100644 --- a/tests/components/shelly/snapshots/test_devices.ambr +++ b/tests/components/shelly/snapshots/test_devices.ambr @@ -709,7 +709,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_slot', + 'translation_key': 'left_slot', 'unique_id': '123456789ABC-cury:0-cury_left', 'unit_of_measurement': None, }) @@ -757,7 +757,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_boost', + 'translation_key': 'left_slot_boost', 'unique_id': '123456789ABC-cury:0-cury_left_boost', 'unit_of_measurement': None, }) @@ -805,7 +805,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_slot', + 'translation_key': 'right_slot', 'unique_id': '123456789ABC-cury:0-cury_right', 'unit_of_measurement': None, }) @@ -853,7 +853,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_boost', + 'translation_key': 'right_slot_boost', 'unique_id': '123456789ABC-cury:0-cury_right_boost', 'unit_of_measurement': None, }) diff --git a/tests/components/shelly/snapshots/test_switch.ambr b/tests/components/shelly/snapshots/test_switch.ambr index 8d79f6904c61a..e376b7c4ad62d 100644 --- a/tests/components/shelly/snapshots/test_switch.ambr +++ b/tests/components/shelly/snapshots/test_switch.ambr @@ -77,7 +77,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_slot', + 'translation_key': 'left_slot', 'unique_id': '123456789ABC-cury:0-cury_left', 'unit_of_measurement': None, }) @@ -125,7 +125,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_boost', + 'translation_key': 'left_slot_boost', 'unique_id': '123456789ABC-cury:0-cury_left_boost', 'unit_of_measurement': None, }) @@ -173,7 +173,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_slot', + 'translation_key': 'right_slot', 'unique_id': '123456789ABC-cury:0-cury_right', 'unit_of_measurement': None, }) @@ -221,7 +221,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'cury_boost', + 'translation_key': 'right_slot_boost', 'unique_id': '123456789ABC-cury:0-cury_right_boost', 'unit_of_measurement': None, }) diff --git a/tests/components/tuya/test_light.py b/tests/components/tuya/test_light.py index 45067f779b732..45f1fe387f134 100644 --- a/tests/components/tuya/test_light.py +++ b/tests/components/tuya/test_light.py @@ -45,9 +45,10 @@ async def test_platform_setup_and_discovery( ["dj_mki13ie507rlry4r"], ) @pytest.mark.parametrize( - ("turn_on_input", "expected_commands"), + ("service", "service_data", "expected_commands"), [ ( + SERVICE_TURN_ON, { ATTR_WHITE: True, }, @@ -58,6 +59,7 @@ async def test_platform_setup_and_discovery( ], ), ( + SERVICE_TURN_ON, { ATTR_BRIGHTNESS: 150, }, @@ -67,6 +69,7 @@ async def test_platform_setup_and_discovery( ], ), ( + SERVICE_TURN_ON, { ATTR_WHITE: True, ATTR_BRIGHTNESS: 150, @@ -78,6 +81,7 @@ async def test_platform_setup_and_discovery( ], ), ( + SERVICE_TURN_ON, { ATTR_WHITE: 150, }, @@ -87,17 +91,23 @@ async def test_platform_setup_and_discovery( {"code": "bright_value_v2", "value": 592}, ], ), + ( + SERVICE_TURN_OFF, + {}, + [{"code": "switch_led", "value": False}], + ), ], ) -async def test_turn_on_white( +async def test_action( hass: HomeAssistant, mock_manager: Manager, mock_config_entry: MockConfigEntry, mock_device: CustomerDevice, - turn_on_input: dict[str, Any], + service: str, + service_data: dict[str, Any], expected_commands: list[dict[str, Any]], ) -> None: - """Test turn_on service.""" + """Test service action.""" entity_id = "light.garage_light" await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) @@ -105,10 +115,10 @@ async def test_turn_on_white( assert state is not None, f"{entity_id} does not exist" await hass.services.async_call( LIGHT_DOMAIN, - SERVICE_TURN_ON, + service, { ATTR_ENTITY_ID: entity_id, - **turn_on_input, + **service_data, }, blocking=True, ) @@ -116,32 +126,3 @@ async def test_turn_on_white( mock_device.id, expected_commands, ) - - -@pytest.mark.parametrize( - "mock_device_code", - ["dj_mki13ie507rlry4r"], -) -async def test_turn_off( - hass: HomeAssistant, - mock_manager: Manager, - mock_config_entry: MockConfigEntry, - mock_device: CustomerDevice, -) -> None: - """Test turn_off service.""" - entity_id = "light.garage_light" - await initialize_entry(hass, mock_manager, mock_config_entry, mock_device) - - state = hass.states.get(entity_id) - assert state is not None, f"{entity_id} does not exist" - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: entity_id, - }, - blocking=True, - ) - mock_manager.send_commands.assert_called_once_with( - mock_device.id, [{"code": "switch_led", "value": False}] - )