mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +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.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
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.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
BringActivityCoordinator,
|
BringActivityCoordinator,
|
||||||
BringConfigEntry,
|
BringConfigEntry,
|
||||||
BringCoordinators,
|
BringCoordinators,
|
||||||
BringDataUpdateCoordinator,
|
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]
|
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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:
|
async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
|
||||||
"""Set up Bring! from a config entry."""
|
"""Set up Bring! from a config entry."""
|
||||||
|
|
||||||
|
@ -7,5 +7,8 @@ DOMAIN = "bring"
|
|||||||
ATTR_SENDER: Final = "sender"
|
ATTR_SENDER: Final = "sender"
|
||||||
ATTR_ITEM_NAME: Final = "item"
|
ATTR_ITEM_NAME: Final = "item"
|
||||||
ATTR_NOTIFICATION_TYPE: Final = "message"
|
ATTR_NOTIFICATION_TYPE: Final = "message"
|
||||||
|
ATTR_REACTION: Final = "reaction"
|
||||||
|
ATTR_ACTIVITY: Final = "uuid"
|
||||||
|
ATTR_RECEIVER: Final = "publicUserUuid"
|
||||||
SERVICE_PUSH_NOTIFICATION = "send_message"
|
SERVICE_PUSH_NOTIFICATION = "send_message"
|
||||||
|
SERVICE_ACTIVITY_STREAM_REACTION = "send_reaction"
|
||||||
|
@ -35,6 +35,9 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"send_message": {
|
"send_message": {
|
||||||
"service": "mdi:cellphone-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
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
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": {
|
"notify_request_failed": {
|
||||||
"message": "Failed to send push notification for Bring! due to a connection error, try again later"
|
"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": {
|
"services": {
|
||||||
@ -164,6 +177,20 @@
|
|||||||
"description": "Item name(s) to include in an urgent message e.g. 'Attention! Attention! - We still urgently need: [Items]'"
|
"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": {
|
"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