diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 8f1b5ae8b1a..6dc4647e42e 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -29,6 +29,8 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper +PARALLEL_UPDATES = 0 + _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[ ESPHomeAlarmControlPanelState, AlarmControlPanelState ] = EsphomeEnumMapper( diff --git a/homeassistant/components/esphome/assist_satellite.py b/homeassistant/components/esphome/assist_satellite.py index 9d92b5fcb92..cf1e299a6f0 100644 --- a/homeassistant/components/esphome/assist_satellite.py +++ b/homeassistant/components/esphome/assist_satellite.py @@ -47,6 +47,8 @@ from .entry_data import ESPHomeConfigEntry, RuntimeEntryData from .enum_mapper import EsphomeEnumMapper from .ffmpeg_proxy import async_create_proxy_url +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[ diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 02b13748fb6..bf773fead0c 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -20,6 +20,8 @@ from .const import DOMAIN from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry from .entry_data import ESPHomeConfigEntry +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index f13fa65ede1..31121d98ff7 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -16,6 +16,8 @@ from .entity import ( platform_async_setup_entry, ) +PARALLEL_UPDATES = 0 + class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): """A button implementation for ESPHome.""" diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 6038bf52e06..e2213153092 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -16,6 +16,8 @@ from homeassistant.core import callback from .entity import EsphomeEntity, platform_async_setup_entry +PARALLEL_UPDATES = 0 + class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]): """A camera implementation for ESPHome.""" diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index b651f16dfd7..3f80f04e527 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -65,6 +65,8 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper +PARALLEL_UPDATES = 0 + FAN_QUIET = "quiet" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 83c749f89ca..4426724e3f4 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -24,6 +24,8 @@ from .entity import ( platform_async_setup_entry, ) +PARALLEL_UPDATES = 0 + class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """A cover implementation for ESPHome.""" diff --git a/homeassistant/components/esphome/date.py b/homeassistant/components/esphome/date.py index 28bc532918a..ef446cceac6 100644 --- a/homeassistant/components/esphome/date.py +++ b/homeassistant/components/esphome/date.py @@ -11,6 +11,8 @@ from homeassistant.components.date import DateEntity from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +PARALLEL_UPDATES = 0 + class EsphomeDate(EsphomeEntity[DateInfo, DateState], DateEntity): """A date implementation for esphome.""" diff --git a/homeassistant/components/esphome/datetime.py b/homeassistant/components/esphome/datetime.py index d1bb0bb77ff..3ea285fa849 100644 --- a/homeassistant/components/esphome/datetime.py +++ b/homeassistant/components/esphome/datetime.py @@ -12,6 +12,8 @@ from homeassistant.util import dt as dt_util from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +PARALLEL_UPDATES = 0 + class EsphomeDateTime(EsphomeEntity[DateTimeInfo, DateTimeState], DateTimeEntity): """A datetime implementation for esphome.""" diff --git a/homeassistant/components/esphome/event.py b/homeassistant/components/esphome/event.py index f4db3844e3d..4437292c5b4 100644 --- a/homeassistant/components/esphome/event.py +++ b/homeassistant/components/esphome/event.py @@ -12,6 +12,8 @@ from homeassistant.util.enum import try_parse_enum from .entity import EsphomeEntity, platform_async_setup_entry +PARALLEL_UPDATES = 0 + class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity): """An event implementation for ESPHome.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index c09145c17b5..7e5922745cc 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -30,6 +30,8 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper +PARALLEL_UPDATES = 0 + ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 8fecf34862b..2593f348680 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -38,6 +38,8 @@ from .entity import ( platform_async_setup_entry, ) +PARALLEL_UPDATES = 0 + FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index 502cd361277..21a76c71b3a 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -18,6 +18,8 @@ from .entity import ( platform_async_setup_entry, ) +PARALLEL_UPDATES = 0 + class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): """A lock implementation for ESPHome.""" diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 5721478c921..62963178a8e 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -44,7 +44,7 @@ from homeassistant.core import ( State, callback, ) -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -827,7 +827,18 @@ def execute_service( entry_data: RuntimeEntryData, service: UserService, call: ServiceCall ) -> None: """Execute a service on a node.""" - entry_data.client.execute_service(service, call.data) + try: + entry_data.client.execute_service(service, call.data) + except APIConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="action_call_failed", + translation_placeholders={ + "call_name": service.name, + "device_name": entry_data.name, + "error": str(err), + }, + ) from err def build_service_name(device_info: EsphomeDeviceInfo, service: UserService) -> str: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 84b7472ad2b..7b0f8083db1 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], "requirements": [ - "aioesphomeapi==29.10.0", + "aioesphomeapi==30.0.1", "esphome-dashboard-api==1.2.3", "bleak-esphome==2.13.1" ], diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 8a30814aa2c..4706ca2ff56 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -41,6 +41,8 @@ from .entity import ( from .enum_mapper import EsphomeEnumMapper from .ffmpeg_proxy import async_create_proxy_url +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) _STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumMapper( diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 2d74dad1bcf..4a6800e1041 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -23,6 +23,8 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper +PARALLEL_UPDATES = 0 + NUMBER_MODES: EsphomeEnumMapper[EsphomeNumberMode, NumberMode] = EsphomeEnumMapper( { EsphomeNumberMode.AUTO: NumberMode.AUTO, diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 67bcbbbd221..f37f774fb1f 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -25,6 +25,8 @@ from .entity import ( ) from .entry_data import ESPHomeConfigEntry, RuntimeEntryData +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 26f33f4fb47..95eabdefa13 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -29,6 +29,8 @@ from homeassistant.util.enum import try_parse_enum from .entity import EsphomeEntity, platform_async_setup_entry from .enum_mapper import EsphomeEnumMapper +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index b7ffb5744d7..bfbedba5a70 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -180,5 +180,10 @@ } } } + }, + "exceptions": { + "action_call_failed": { + "message": "Failed to execute the action call {call_name} on {device_name}: {error}" + } } } diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index c210ae1440b..96b2a426869 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -18,6 +18,8 @@ from .entity import ( platform_async_setup_entry, ) +PARALLEL_UPDATES = 0 + class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """A switch implementation for ESPHome.""" diff --git a/homeassistant/components/esphome/text.py b/homeassistant/components/esphome/text.py index 36d77aac4a0..c36621b8f4e 100644 --- a/homeassistant/components/esphome/text.py +++ b/homeassistant/components/esphome/text.py @@ -17,6 +17,8 @@ from .entity import ( ) from .enum_mapper import EsphomeEnumMapper +PARALLEL_UPDATES = 0 + TEXT_MODES: EsphomeEnumMapper[EsphomeTextMode, TextMode] = EsphomeEnumMapper( { EsphomeTextMode.TEXT: TextMode.TEXT, diff --git a/homeassistant/components/esphome/time.py b/homeassistant/components/esphome/time.py index 477c47cf636..b0e586e1792 100644 --- a/homeassistant/components/esphome/time.py +++ b/homeassistant/components/esphome/time.py @@ -11,6 +11,8 @@ from homeassistant.components.time import TimeEntity from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +PARALLEL_UPDATES = 0 + class EsphomeTime(EsphomeEntity[TimeInfo, TimeState], TimeEntity): """A time implementation for esphome.""" diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 60d4989063b..0874007ecdf 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -38,6 +38,8 @@ from .entity import ( ) from .entry_data import RuntimeEntryData +PARALLEL_UPDATES = 0 + KEY_UPDATE_LOCK = "esphome_update_lock" NO_FEATURES = UpdateEntityFeature(0) diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py index d779a6abb9f..e366fc08d19 100644 --- a/homeassistant/components/esphome/valve.py +++ b/homeassistant/components/esphome/valve.py @@ -22,6 +22,8 @@ from .entity import ( platform_async_setup_entry, ) +PARALLEL_UPDATES = 0 + class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity): """A valve implementation for ESPHome.""" diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 8ae09b58957..add75f5e95b 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -197,7 +197,12 @@ class OAuth2FlowHandler( "Error reading primary calendar, make sure Google Calendar API is enabled: %s", err, ) - return self.async_abort(reason="api_disabled") + return self.async_abort( + reason="calendar_api_disabled", + description_placeholders={ + "calendar_api_url": "https://console.cloud.google.com/apis/library/calendar-json.googleapis.com" + }, + ) except ApiException as err: _LOGGER.error("Error reading primary calendar: %s", err) return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 5776fd0480b..4f3e27af27e 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -28,7 +28,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", "code_expired": "Authentication code expired or credential setup is invalid, please try again.", - "api_disabled": "You must enable the Google Calendar API in the Google Cloud Console" + "calendar_api_disabled": "You must [enable the Google Calendar API]({calendar_api_url}) in the Google Cloud Console" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index 3c2f05fa535..0656454bb01 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -18,7 +18,7 @@ }, "data_description": { "host": "The IP address or hostname of your LaMetric TIME on your network.", - "api_key": "You can find this API key in [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)." + "api_key": "You can find this API key in the [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)." } }, "cloud_select_device": { @@ -83,8 +83,8 @@ "brightness_mode": { "name": "Brightness mode", "state": { - "auto": "Automatic", - "manual": "Manual" + "auto": "[%key:common::state::auto%]", + "manual": "[%key:common::state::manual%]" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 0a814575271..a7096b9ec61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==29.10.0 +aioesphomeapi==30.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82575692d08..2b1a8c4f6b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==29.10.0 +aioesphomeapi==30.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/script/licenses.py b/script/licenses.py index 62e1845b911..ab8ab62eb1d 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -88,6 +88,7 @@ OSI_APPROVED_LICENSES_SPDX = { "MPL-1.1", "MPL-2.0", "PSF-2.0", + "Python-2.0", "Unlicense", "Zlib", "ZPL-2.1", diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index c3913c3ba9b..4f46e4ddc0e 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -106,6 +106,7 @@ async def test_restore_dashboard_storage_skipped_if_addon_uninstalled( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED + await hass.async_block_till_done() # wait for dashboard setup assert "test-slug is no longer installed" in caplog.text assert not mock_dashboard_api.called diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 37ad7cb8f7f..c897377f719 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -42,6 +42,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.setup import async_setup_component @@ -1123,6 +1124,65 @@ async def test_esphome_user_services_ignores_invalid_arg_types( assert not hass.services.has_service(DOMAIN, "with_dash_bad_service") +async def test_esphome_user_service_fails( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test executing a user service fails due to disconnect.""" + entity_info = [] + states = [] + service1 = UserService( + name="simple_service", + key=2, + args=[ + UserServiceArg(name="arg1", type=UserServiceArgType.BOOL), + ], + ) + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=[service1], + device_info={"name": "with-dash"}, + states=states, + ) + await hass.async_block_till_done() + assert hass.services.has_service(DOMAIN, "with_dash_simple_service") + + mock_client.execute_service = Mock(side_effect=APIConnectionError("fail")) + with pytest.raises(HomeAssistantError) as exc: + await hass.services.async_call( + DOMAIN, "with_dash_simple_service", {"arg1": True}, blocking=True + ) + assert exc.value.translation_domain == DOMAIN + assert exc.value.translation_key == "action_call_failed" + assert exc.value.translation_placeholders == { + "call_name": "simple_service", + "device_name": "with-dash", + "error": "fail", + } + assert ( + str(exc.value) + == "Failed to execute the action call simple_service on with-dash: fail" + ) + + mock_client.execute_service.assert_has_calls( + [ + call( + UserService( + name="simple_service", + key=2, + args=[UserServiceArg(name="arg1", type=UserServiceArgType.BOOL)], + ), + {"arg1": True}, + ) + ] + ) + + async def test_esphome_user_services_changes( hass: HomeAssistant, mock_client: APIClient, diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index de882a6f791..e5f4e512579 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -570,7 +570,7 @@ async def test_reauth_flow( ("primary_calendar_error", "primary_calendar_status", "reason"), [ (ClientError(), None, "cannot_connect"), - (None, HTTPStatus.FORBIDDEN, "api_disabled"), + (None, HTTPStatus.FORBIDDEN, "calendar_api_disabled"), (None, HTTPStatus.SERVICE_UNAVAILABLE, "cannot_connect"), ], )