Skip to content

Commit 3377e90

Browse files
authored
Show rotating category name in event summary if pickup is scheduled in ridwell (#152529)
1 parent 342c7f6 commit 3377e90

File tree

7 files changed

+315
-16
lines changed

7 files changed

+315
-16
lines changed

homeassistant/components/ridwell/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
2020
coordinator = RidwellDataUpdateCoordinator(hass, entry)
2121
await coordinator.async_initialize()
2222
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
23+
entry.async_on_unload(entry.add_update_listener(options_update_listener))
2324

2425
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
2526

2627
return True
2728

2829

30+
async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
31+
"""Handle options update."""
32+
await hass.config_entries.async_reload(entry.entry_id)
33+
34+
2935
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
3036
"""Unload a config entry."""
3137
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):

homeassistant/components/ridwell/calendar.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,60 @@
44

55
import datetime
66

7-
from aioridwell.model import RidwellAccount, RidwellPickupEvent
7+
from aioridwell.model import PickupCategory, RidwellAccount, RidwellPickupEvent
88

99
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
1010
from homeassistant.config_entries import ConfigEntry
1111
from homeassistant.core import HomeAssistant, callback
1212
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1313

14-
from .const import DOMAIN
14+
from .const import (
15+
CALENDAR_TITLE_NONE,
16+
CALENDAR_TITLE_ROTATING,
17+
CALENDAR_TITLE_STATUS,
18+
CONF_CALENDAR_TITLE,
19+
DOMAIN,
20+
)
1521
from .coordinator import RidwellDataUpdateCoordinator
1622
from .entity import RidwellEntity
1723

1824

1925
@callback
2026
def async_get_calendar_event_from_pickup_event(
21-
pickup_event: RidwellPickupEvent,
27+
pickup_event: RidwellPickupEvent, config_entry: ConfigEntry
2228
) -> CalendarEvent:
2329
"""Get a HASS CalendarEvent from an aioridwell PickupEvent."""
24-
pickup_type_string = ", ".join(
25-
[
26-
f"{pickup.name} (quantity: {pickup.quantity})"
27-
for pickup in pickup_event.pickups
28-
]
29-
)
30+
pickup_items = []
31+
rotating_category = ""
32+
calendar_preference = config_entry.options.get(CONF_CALENDAR_TITLE, False)
33+
for pickup in pickup_event.pickups:
34+
pickup_items.append(f"{pickup.name} (quantity: {pickup.quantity})")
35+
if pickup.category == PickupCategory.ROTATING:
36+
rotating_category = pickup.name
37+
break
38+
39+
pickup_type_string = ", ".join(pickup_items)
40+
pickup_event_state = pickup_event.state.value
41+
summary_base = "Ridwell Pickup"
42+
43+
if calendar_preference == CALENDAR_TITLE_STATUS:
44+
# Use state of pickup_event (e.g., scheduled, skipped, notified, etc).
45+
summary = f"{summary_base} ({pickup_event_state})"
46+
elif calendar_preference == CALENDAR_TITLE_ROTATING:
47+
# Use name of Rotating Category for the pickup_event.
48+
if rotating_category:
49+
summary = f"{summary_base} ({rotating_category})"
50+
else:
51+
summary = f"{summary_base} ({pickup_event_state})"
52+
elif calendar_preference == CALENDAR_TITLE_NONE:
53+
# Include only a basic title for the event.
54+
summary = summary_base
55+
else:
56+
# Default to pickup status if no selection is made (e.g., scheduled, skipped, etc).
57+
summary = f"{summary_base} ({pickup_event_state})"
58+
3059
return CalendarEvent(
31-
summary=f"Ridwell Pickup ({pickup_event.state.value})",
60+
summary=summary,
3261
description=f"Pickup types: {pickup_type_string}",
3362
start=pickup_event.pickup_date,
3463
end=pickup_event.pickup_date + datetime.timedelta(days=1),
@@ -67,7 +96,9 @@ def __init__(
6796
@property
6897
def event(self) -> CalendarEvent | None:
6998
"""Return the next upcoming event."""
70-
return async_get_calendar_event_from_pickup_event(self.next_pickup_event)
99+
return async_get_calendar_event_from_pickup_event(
100+
self.next_pickup_event, self.coordinator.config_entry
101+
)
71102

72103
async def async_get_events(
73104
self,
@@ -77,6 +108,8 @@ async def async_get_events(
77108
) -> list[CalendarEvent]:
78109
"""Return calendar events within a datetime range."""
79110
return [
80-
async_get_calendar_event_from_pickup_event(event)
111+
async_get_calendar_event_from_pickup_event(
112+
event, self.coordinator.config_entry
113+
)
81114
for event in self.coordinator.data[self._account.account_id]
82115
]

homeassistant/components/ridwell/config_flow.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
from aioridwell.errors import InvalidCredentialsError, RidwellError
1010
import voluptuous as vol
1111

12-
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
12+
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
1313
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
14-
from homeassistant.helpers import aiohttp_client, config_validation as cv
14+
from homeassistant.core import callback
15+
from homeassistant.helpers import aiohttp_client, config_validation as cv, selector
16+
from homeassistant.helpers.schema_config_entry_flow import (
17+
SchemaFlowFormStep,
18+
SchemaOptionsFlowHandler,
19+
)
1520

16-
from .const import DOMAIN, LOGGER
21+
from .const import CALENDAR_TITLE_OPTIONS, CONF_CALENDAR_TITLE, DOMAIN, LOGGER
1722

1823
STEP_REAUTH_CONFIRM_DATA_SCHEMA = vol.Schema(
1924
{
@@ -28,6 +33,24 @@
2833
}
2934
)
3035

36+
OPTIONS_SCHEMA = vol.Schema(
37+
{
38+
vol.Optional(CONF_CALENDAR_TITLE): selector.SelectSelector(
39+
selector.SelectSelectorConfig(
40+
options=CALENDAR_TITLE_OPTIONS,
41+
multiple=False,
42+
mode=selector.SelectSelectorMode.LIST,
43+
translation_key=CONF_CALENDAR_TITLE,
44+
),
45+
)
46+
}
47+
)
48+
49+
50+
OPTIONS_FLOW = {
51+
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
52+
}
53+
3154

3255
class RidwellConfigFlow(ConfigFlow, domain=DOMAIN):
3356
"""Handle a config flow for Ridwell."""
@@ -81,6 +104,23 @@ async def _async_validate(
81104
data={CONF_USERNAME: self._username, CONF_PASSWORD: self._password},
82105
)
83106

107+
@staticmethod
108+
@callback
109+
def async_get_options_flow(
110+
config_entry: ConfigEntry,
111+
) -> SchemaOptionsFlowHandler:
112+
"""Get options flow for this handler."""
113+
try:
114+
schema_options_flow_handler = SchemaOptionsFlowHandler(
115+
config_entry, OPTIONS_FLOW
116+
)
117+
except SystemError as err:
118+
LOGGER.error("Unknown System error: %s", err)
119+
except RidwellError as err:
120+
LOGGER.error("Unknown Ridwell error: %s", err)
121+
122+
return schema_options_flow_handler
123+
84124
async def async_step_reauth(
85125
self, entry_data: Mapping[str, Any]
86126
) -> ConfigFlowResult:

homeassistant/components/ridwell/const.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@
77
LOGGER = logging.getLogger(__package__)
88

99
SENSOR_TYPE_NEXT_PICKUP = "next_pickup"
10+
11+
CONF_CALENDAR_TITLE = "conf_calendar_title"
12+
13+
CALENDAR_TITLE_STATUS = "pickup_status"
14+
CALENDAR_TITLE_ROTATING = "rotating_category"
15+
CALENDAR_TITLE_NONE = "no_detail"
16+
17+
CALENDAR_TITLE_OPTIONS = [
18+
CALENDAR_TITLE_STATUS,
19+
CALENDAR_TITLE_ROTATING,
20+
CALENDAR_TITLE_NONE,
21+
]

homeassistant/components/ridwell/strings.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,27 @@
3636
"name": "Opt-in to next pickup"
3737
}
3838
}
39+
},
40+
"options": {
41+
"step": {
42+
"init": {
43+
"description": "Customize how Ridwell events appear in Home Assistant.",
44+
"data": {
45+
"conf_calendar_title": "Event summary details"
46+
},
47+
"data_description": {
48+
"conf_calendar_title": "Details included (in parentheses) after the event summary/title can be the pickup state, the name of the next rotating category, or no details at all."
49+
}
50+
}
51+
}
52+
},
53+
"selector": {
54+
"conf_calendar_title": {
55+
"options": {
56+
"no_detail": "No details",
57+
"pickup_status": "Pickup status",
58+
"rotating_category": "Rotating category"
59+
}
60+
}
3961
}
4062
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""Test Ridwell calendar platform."""
2+
3+
from datetime import date
4+
5+
from aioridwell.model import EventState, RidwellPickup, RidwellPickupEvent
6+
import pytest
7+
8+
from homeassistant.components.calendar import CalendarEvent
9+
from homeassistant.components.ridwell.calendar import (
10+
async_get_calendar_event_from_pickup_event,
11+
)
12+
from homeassistant.components.ridwell.const import (
13+
CALENDAR_TITLE_NONE,
14+
CALENDAR_TITLE_ROTATING,
15+
CALENDAR_TITLE_STATUS,
16+
CONF_CALENDAR_TITLE,
17+
)
18+
from homeassistant.core import HomeAssistant
19+
20+
START_DATE = date(2025, 10, 4)
21+
END_DATE = date(2025, 10, 5)
22+
23+
24+
@pytest.mark.asyncio
25+
@pytest.mark.parametrize(
26+
(
27+
"pickup_name",
28+
"event_state",
29+
"summary_style",
30+
"expected_description",
31+
"expected_summary",
32+
),
33+
[
34+
# Valid events that should be converted.
35+
# Pickup name of "Cork" is used to produce PickupCategory.ROTATING,
36+
# and "Plastic Film" is used to generate a PickupCategory.STANDARD pickup.
37+
(
38+
"Cork",
39+
EventState.SCHEDULED,
40+
CALENDAR_TITLE_ROTATING, # Display Rotating Category in summary.
41+
"Pickup types: Cork (quantity: 1)",
42+
"Ridwell Pickup (Cork)",
43+
),
44+
(
45+
"Cork",
46+
EventState.SCHEDULED,
47+
CALENDAR_TITLE_NONE, # Display no extra info in summary.
48+
"Pickup types: Cork (quantity: 1)",
49+
"Ridwell Pickup",
50+
),
51+
(
52+
"Cork",
53+
EventState.INITIALIZED,
54+
CALENDAR_TITLE_ROTATING, # Display Rotating Category in summary.
55+
"Pickup types: Cork (quantity: 1)",
56+
"Ridwell Pickup (Cork)",
57+
),
58+
(
59+
"Cork",
60+
EventState.SKIPPED,
61+
CALENDAR_TITLE_STATUS, # Display pickup state in summary.
62+
"Pickup types: Cork (quantity: 1)",
63+
"Ridwell Pickup (skipped)",
64+
),
65+
(
66+
"Cork",
67+
EventState.INITIALIZED,
68+
CALENDAR_TITLE_STATUS, # Display pickup state in summary.
69+
"Pickup types: Cork (quantity: 1)",
70+
"Ridwell Pickup (initialized)",
71+
),
72+
(
73+
"Cork",
74+
EventState.UNKNOWN,
75+
CALENDAR_TITLE_STATUS, # Display pickup state in summary.
76+
"Pickup types: Cork (quantity: 1)",
77+
"Ridwell Pickup (unknown)",
78+
),
79+
],
80+
)
81+
async def test_calendar_event_varied_states_and_types(
82+
hass: HomeAssistant,
83+
config_entry,
84+
pickup_name: str,
85+
event_state: EventState,
86+
expected_description: str,
87+
expected_summary: str,
88+
summary_style: str,
89+
) -> None:
90+
"""Test CalendarEvent output based on pickup type and event state."""
91+
92+
# Set calendar config to default
93+
hass.config_entries.async_update_entry(
94+
config_entry,
95+
options={CONF_CALENDAR_TITLE: summary_style},
96+
)
97+
await hass.async_block_till_done()
98+
99+
# Create test pickup
100+
pickup = RidwellPickup(
101+
name=pickup_name,
102+
offer_id=f"offer_{pickup_name.lower()}",
103+
quantity=1,
104+
product_id=f"product_{pickup_name.lower()}",
105+
priority=1,
106+
)
107+
108+
# Create test pickup event with the given state
109+
pickup_event = RidwellPickupEvent(
110+
_async_request=None,
111+
event_id=f"event_{pickup_name.lower()}_{event_state.name.lower()}",
112+
pickup_date=START_DATE,
113+
pickups=[pickup],
114+
state=event_state,
115+
)
116+
117+
# Call the function under test
118+
event = async_get_calendar_event_from_pickup_event(pickup_event, config_entry)
119+
120+
assert isinstance(event, CalendarEvent)
121+
assert event.summary == expected_summary
122+
assert event.description == expected_description
123+
assert event.start == START_DATE
124+
assert event.end == END_DATE
125+
126+
127+
async def test_calendar_event_with_no_pickups(
128+
hass: HomeAssistant,
129+
config_entry,
130+
) -> None:
131+
"""Test empty pickups."""
132+
pickup_event = RidwellPickupEvent(
133+
_async_request=None,
134+
event_id="event_empty",
135+
pickup_date=START_DATE,
136+
pickups=[],
137+
state=EventState.SCHEDULED,
138+
)
139+
140+
event = async_get_calendar_event_from_pickup_event(pickup_event, config_entry)
141+
assert event.description == "Pickup types: "

0 commit comments

Comments
 (0)