mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
Add action for activity reactions to Bring! (#138175)
This commit is contained in:
parent
3307132441
commit
15544769b6
@ -8,20 +8,33 @@ from bring_api import Bring
|
||||
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
BringActivityCoordinator,
|
||||
BringConfigEntry,
|
||||
BringCoordinators,
|
||||
BringDataUpdateCoordinator,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Bring! services."""
|
||||
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
|
||||
"""Set up Bring! from a config entry."""
|
||||
|
||||
|
@ -7,5 +7,8 @@ DOMAIN = "bring"
|
||||
ATTR_SENDER: Final = "sender"
|
||||
ATTR_ITEM_NAME: Final = "item"
|
||||
ATTR_NOTIFICATION_TYPE: Final = "message"
|
||||
|
||||
ATTR_REACTION: Final = "reaction"
|
||||
ATTR_ACTIVITY: Final = "uuid"
|
||||
ATTR_RECEIVER: Final = "publicUserUuid"
|
||||
SERVICE_PUSH_NOTIFICATION = "send_message"
|
||||
SERVICE_ACTIVITY_STREAM_REACTION = "send_reaction"
|
||||
|
@ -35,6 +35,9 @@
|
||||
"services": {
|
||||
"send_message": {
|
||||
"service": "mdi:cellphone-message"
|
||||
},
|
||||
"send_reaction": {
|
||||
"service": "mdi:thumb-up"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
110
homeassistant/components/bring/services.py
Normal file
110
homeassistant/components/bring/services.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""Actions for Bring! integration."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bring_api import (
|
||||
ActivityType,
|
||||
BringAuthException,
|
||||
BringNotificationType,
|
||||
BringRequestException,
|
||||
ReactionType,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.event import ATTR_EVENT_TYPE
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
|
||||
from .const import (
|
||||
ATTR_ACTIVITY,
|
||||
ATTR_REACTION,
|
||||
ATTR_RECEIVER,
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
)
|
||||
from .coordinator import BringConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_ACTIVITY_STREAM_REACTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_REACTION): vol.All(
|
||||
vol.Upper,
|
||||
vol.Coerce(ReactionType),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_config_entry(hass: HomeAssistant, entry_id: str) -> BringConfigEntry:
|
||||
"""Return config entry or raise if not found or not loaded."""
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if TYPE_CHECKING:
|
||||
assert entry
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_loaded",
|
||||
)
|
||||
return entry
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services for Bring! integration."""
|
||||
|
||||
async def async_send_activity_stream_reaction(call: ServiceCall) -> None:
|
||||
"""Send a reaction in response to recent activity of a list member."""
|
||||
|
||||
if (
|
||||
not (state := hass.states.get(call.data[ATTR_ENTITY_ID]))
|
||||
or not (entity := er.async_get(hass).async_get(call.data[ATTR_ENTITY_ID]))
|
||||
or not entity.config_entry_id
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entity_not_found",
|
||||
translation_placeholders={
|
||||
ATTR_ENTITY_ID: call.data[ATTR_ENTITY_ID],
|
||||
},
|
||||
)
|
||||
config_entry = get_config_entry(hass, entity.config_entry_id)
|
||||
|
||||
coordinator = config_entry.runtime_data.data
|
||||
|
||||
list_uuid = entity.unique_id.split("_")[1]
|
||||
|
||||
activity = state.attributes[ATTR_EVENT_TYPE]
|
||||
|
||||
reaction: ReactionType = call.data[ATTR_REACTION]
|
||||
|
||||
if not activity:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="activity_not_found",
|
||||
)
|
||||
try:
|
||||
await coordinator.bring.notify(
|
||||
list_uuid,
|
||||
BringNotificationType.LIST_ACTIVITY_STREAM_REACTION,
|
||||
receiver=state.attributes[ATTR_RECEIVER],
|
||||
activity=state.attributes[ATTR_ACTIVITY],
|
||||
activity_type=ActivityType(activity.upper()),
|
||||
reaction=reaction,
|
||||
)
|
||||
except (BringRequestException, BringAuthException) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="reaction_request_failed",
|
||||
) from e
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
async_send_activity_stream_reaction,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION_SCHEMA,
|
||||
)
|
@ -21,3 +21,28 @@ send_message:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
send_reaction:
|
||||
fields:
|
||||
entity_id:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- integration: bring
|
||||
domain: event
|
||||
example: event.shopping_list
|
||||
reaction:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: 👍🏼
|
||||
value: thumbs_up
|
||||
- label: 🧐
|
||||
value: monocle
|
||||
- label: 🤤
|
||||
value: drooling
|
||||
- label: ❤️
|
||||
value: heart
|
||||
mode: dropdown
|
||||
example: thumbs_up
|
||||
|
@ -144,6 +144,19 @@
|
||||
},
|
||||
"notify_request_failed": {
|
||||
"message": "Failed to send push notification for Bring! due to a connection error, try again later"
|
||||
},
|
||||
"reaction_request_failed": {
|
||||
"message": "Failed to send reaction for Bring! due to a connection error, try again later"
|
||||
},
|
||||
"activity_not_found": {
|
||||
"message": "Failed to send reaction for Bring! — No recent activity found"
|
||||
},
|
||||
"entity_not_found": {
|
||||
"message": "Failed to send reaction for Bring! — Unknown entity {entity_id}"
|
||||
},
|
||||
|
||||
"entry_not_loaded": {
|
||||
"message": "The account associated with this Bring! list is either not loaded or disabled in Home Assistant."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
@ -164,6 +177,20 @@
|
||||
"description": "Item name(s) to include in an urgent message e.g. 'Attention! Attention! - We still urgently need: [Items]'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"send_reaction": {
|
||||
"name": "Send reaction",
|
||||
"description": "Sends a reaction to a recent activity on a Bring! list by a member of the shared list.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Activities",
|
||||
"description": "Select the Bring! activities event entity for reacting to its most recent event"
|
||||
},
|
||||
"reaction": {
|
||||
"name": "Reaction",
|
||||
"description": "Type of reaction to send in response."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
190
tests/components/bring/test_services.py
Normal file
190
tests/components/bring/test_services.py
Normal file
@ -0,0 +1,190 @@
|
||||
"""Test actions of Bring! integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from bring_api import (
|
||||
ActivityType,
|
||||
BringActivityResponse,
|
||||
BringNotificationType,
|
||||
BringRequestException,
|
||||
ReactionType,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bring.const import (
|
||||
ATTR_REACTION,
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("reaction", "call_arg"),
|
||||
[
|
||||
("drooling", ReactionType.DROOLING),
|
||||
("heart", ReactionType.HEART),
|
||||
("monocle", ReactionType.MONOCLE),
|
||||
("thumbs_up", ReactionType.THUMBS_UP),
|
||||
],
|
||||
)
|
||||
async def test_send_reaction(
|
||||
hass: HomeAssistant,
|
||||
bring_config_entry: MockConfigEntry,
|
||||
mock_bring_client: AsyncMock,
|
||||
reaction: str,
|
||||
call_arg: ReactionType,
|
||||
) -> None:
|
||||
"""Test send activity stream reaction."""
|
||||
|
||||
bring_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(bring_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert bring_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: reaction,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_bring_client.notify.assert_called_once_with(
|
||||
"e542eef6-dba7-4c31-a52c-29e6ab9d83a5",
|
||||
BringNotificationType.LIST_ACTIVITY_STREAM_REACTION,
|
||||
receiver="9a21fdfc-63a4-441a-afc1-ef3030605a9d",
|
||||
activity="673594a9-f92d-4cb6-adf1-d2f7a83207a4",
|
||||
activity_type=ActivityType.LIST_ITEMS_CHANGED,
|
||||
reaction=call_arg,
|
||||
)
|
||||
|
||||
|
||||
async def test_send_reaction_exception(
|
||||
hass: HomeAssistant,
|
||||
bring_config_entry: MockConfigEntry,
|
||||
mock_bring_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test send activity stream reaction with exception."""
|
||||
|
||||
bring_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(bring_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert bring_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_bring_client.notify.side_effect = BringRequestException
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Failed to send reaction for Bring! due to a connection error, try again later",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_bring_client")
|
||||
async def test_send_reaction_config_entry_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
bring_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test send activity stream reaction config entry not loaded exception."""
|
||||
|
||||
bring_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(bring_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.config_entries.async_unload(bring_config_entry.entry_id)
|
||||
|
||||
assert bring_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="The account associated with this Bring! list is either not loaded or disabled in Home Assistant",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_bring_client")
|
||||
async def test_send_reaction_unknown_entity(
|
||||
hass: HomeAssistant,
|
||||
bring_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test send activity stream reaction unknown entity exception."""
|
||||
|
||||
bring_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(bring_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert bring_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity_registry.async_update_entity(
|
||||
"event.einkauf_activities", disabled_by=er.RegistryEntryDisabler.USER
|
||||
)
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match="Failed to send reaction for Bring! — Unknown entity event.einkauf_activities",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_send_reaction_not_found(
|
||||
hass: HomeAssistant,
|
||||
bring_config_entry: MockConfigEntry,
|
||||
mock_bring_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test send activity stream reaction not found validation error."""
|
||||
mock_bring_client.get_activity.return_value = BringActivityResponse.from_dict(
|
||||
{"timeline": [], "timestamp": "2025-01-01T03:09:33.036Z", "totalEvents": 0}
|
||||
)
|
||||
|
||||
bring_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(bring_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert bring_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Failed to send reaction for Bring! — No recent activity found",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_ACTIVITY_STREAM_REACTION,
|
||||
service_data={
|
||||
ATTR_ENTITY_ID: "event.einkauf_activities",
|
||||
ATTR_REACTION: "heart",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user