Merge branch 'dev' into esphome_bronze

This commit is contained in:
J. Nick Koston 2025-04-15 21:39:54 -10:00 committed by GitHub
commit 03f6eda8b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 138 additions and 11 deletions

View File

@ -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(

View File

@ -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[

View File

@ -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,

View File

@ -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."""

View File

@ -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."""

View File

@ -65,6 +65,8 @@ from .entity import (
)
from .enum_mapper import EsphomeEnumMapper
PARALLEL_UPDATES = 0
FAN_QUIET = "quiet"

View File

@ -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."""

View File

@ -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."""

View File

@ -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."""

View File

@ -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."""

View File

@ -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]

View File

@ -38,6 +38,8 @@ from .entity import (
platform_async_setup_entry,
)
PARALLEL_UPDATES = 0
FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10}

View File

@ -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."""

View File

@ -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:

View File

@ -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"
],

View File

@ -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(

View File

@ -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,

View File

@ -25,6 +25,8 @@ from .entity import (
)
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,

View File

@ -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,

View File

@ -180,5 +180,10 @@
}
}
}
},
"exceptions": {
"action_call_failed": {
"message": "Failed to execute the action call {call_name} on {device_name}: {error}"
}
}
}

View File

@ -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."""

View File

@ -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,

View File

@ -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."""

View File

@ -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)

View File

@ -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."""

View File

@ -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")

View File

@ -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%]"

View File

@ -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%]"
}
}
},

2
requirements_all.txt generated
View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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,

View File

@ -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"),
],
)