Skip to content

Commit 7aaece9

Browse files
authored
Merge pull request #6 from gensyn/copilot/feature-expose-additional-configuration-services
Add input helper overrides for active state, task interval, and todo offset
2 parents b29eb73 + b067d38 commit 7aaece9

11 files changed

Lines changed: 354 additions & 41 deletions

File tree

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,19 @@ Access task settings through the cog icon ⚙️ on the integration page.
7171
| Option | Description |
7272
|--------|-------------------------------------------------------------------------------------------|
7373
| **Active** | Pause tasks when disabled (sensor shows `inactive` state) |
74+
| **Active Override** | Select an `input_boolean` helper to override the Active setting at runtime |
7475
| **Task Interval & Unit** | Modify how often the task repeats |
76+
| **Task Interval Override** | Select an `input_number` helper (value in days) to override the Task Interval at runtime |
7577
| **Material Design Icon** | Choose an icon for the sensor (available as attribute for notifications) |
7678
| **Tags** | Add keywords for filtering in automations/templates (e.g., assignees, notification times) |
77-
| **Todo Lists** | Select Local Todo lists for automatic task addition when due |
79+
| **Todo Lists** | Select Todo lists for automatic task addition when due |
7880
| **Todo List Offset** | Add task to lists `n` days before due date |
81+
| **Todo List Offset Override** | Select an `input_number` helper (value in days) to override the Todo List Offset at runtime |
7982
| **Notification Interval** | Reference value for automation/template notification timing |
8083

81-
> **Note:** Tags and notification intervals require you to implement filtering logic in your own automations.
84+
> **Note:** Tags and notification intervals require you to implement filtering logic in your own automations.
85+
>
86+
> **Override fields:** When an override helper is selected, its current value takes precedence over the configured option. If the helper is `unavailable` or `unknown`, the configured value is used as a fallback. Override values react to helper state changes in real time, allowing non-admin users to adjust task behaviour through dashboard tiles or scripts without needing access to integration settings.
8287
8388
---
8489

__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from homeassistant.helpers.typing import ConfigType
1414
from .const import DOMAIN, CONF_TASK_INTERVAL_VALUE, CONF_DAY, CONF_TASK_INTERVAL_TYPE, CONF_NOTIFICATION_INTERVAL, \
1515
CONF_TODO_OFFSET_DAYS, CONF_TAGS, CONF_ACTIVE, CONF_TODO_LISTS, SERVICE_MARK_AS_DONE, \
16-
SERVICE_MARK_AS_DONE_SCHEMA, SERVICE_SET_LAST_DONE_DATE, SERVICE_SET_LAST_DONE_DATE_SCHEMA, CONF_DATE
16+
SERVICE_MARK_AS_DONE_SCHEMA, SERVICE_SET_LAST_DONE_DATE, SERVICE_SET_LAST_DONE_DATE_SCHEMA, CONF_DATE, \
17+
CONF_ACTIVE_OVERRIDE, CONF_TASK_INTERVAL_OVERRIDE, CONF_TODO_OFFSET_OVERRIDE
1718
from .frontend import TaskTrackerCardRegistration
1819
from .sensor import TaskTrackerSensor
1920

assets/3_options.png

49 KB
Loading

const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
CONF_DROPDOWN = "dropdown"
1414
CONF_TAGS = "tags"
1515
CONF_ACTIVE = "active"
16+
CONF_ACTIVE_OVERRIDE = "active_override"
17+
CONF_TASK_INTERVAL_OVERRIDE = "task_interval_override"
18+
CONF_TODO_OFFSET_OVERRIDE = "todo_offset_override"
1619
CONF_SELECT = "select"
1720
CONF_DAY = "day"
1821
CONF_WEEK = "week"

options_flow.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,28 @@
22

33
import voluptuous as vol
44

5-
from homeassistant.components.local_todo.const import DOMAIN as LOCAL_TODO_DOMAIN
65
from homeassistant.config_entries import OptionsFlowWithReload, ConfigFlowResult
76
from homeassistant.const import CONF_ICON, CONF_OPTIONS, CONF_MODE
8-
from homeassistant.core import HomeAssistant
9-
from homeassistant.helpers import entity_registry
107
from homeassistant.helpers.selector import selector
118
from .const import CONF_TASK_INTERVAL_VALUE, CONF_NOTIFICATION_INTERVAL, CONF_TAGS, CONF_ACTIVE, \
129
CONF_TASK_INTERVAL_TYPE, CONF_TODO_OFFSET_DAYS, CONF_SELECT, CONF_DAY, CONF_WEEK, CONF_MONTH, CONF_YEAR, \
13-
CONF_DROPDOWN, CONF_TODO_LISTS
10+
CONF_DROPDOWN, CONF_TODO_LISTS, CONF_ACTIVE_OVERRIDE, CONF_TASK_INTERVAL_OVERRIDE, CONF_TODO_OFFSET_OVERRIDE
1411

1512

1613
class TaskTrackerOptionsFlow(OptionsFlowWithReload):
1714
async def async_step_init(
1815
self, user_input: dict[str, Any] | None = None
1916
) -> ConfigFlowResult:
2017
"""Manage the options."""
21-
todo_lists = await get_todo_lists(self.hass)
2218

2319
STEP_INIT_SCHEMA = vol.Schema(
2420
{
2521
vol.Optional(CONF_ACTIVE): bool,
22+
vol.Optional(CONF_ACTIVE_OVERRIDE): selector({
23+
"entity": {
24+
"domain": "input_boolean",
25+
}
26+
}),
2627
vol.Required(CONF_TASK_INTERVAL_VALUE, default=7): int,
2728
vol.Required(CONF_TASK_INTERVAL_TYPE): selector({
2829
CONF_SELECT: {
@@ -31,17 +32,25 @@ async def async_step_init(
3132
"translation_key": "task_interval",
3233
}
3334
}),
35+
vol.Optional(CONF_TASK_INTERVAL_OVERRIDE): selector({
36+
"entity": {
37+
"domain": "input_number",
38+
}
39+
}),
3440
vol.Optional(CONF_ICON, default="mdi:calendar-question"): str,
3541
vol.Optional(CONF_TAGS): str,
3642
vol.Optional(CONF_TODO_LISTS): selector({
37-
CONF_SELECT: {
38-
CONF_OPTIONS: [{"value": todo[0], "label": todo[1]} for todo in todo_lists],
39-
CONF_MODE: CONF_DROPDOWN,
40-
"translation_key": "task_interval",
43+
"entity": {
44+
"domain": "todo",
4145
"multiple": True,
4246
}
4347
}),
4448
vol.Optional(CONF_TODO_OFFSET_DAYS, default=0): int,
49+
vol.Optional(CONF_TODO_OFFSET_OVERRIDE): selector({
50+
"entity": {
51+
"domain": "input_number",
52+
}
53+
}),
4554
vol.Optional(CONF_NOTIFICATION_INTERVAL, default=1): int,
4655
}
4756
)
@@ -59,14 +68,6 @@ async def async_step_init(
5968
return self.async_create_entry(data=options)
6069

6170

62-
async def get_todo_lists(hass: HomeAssistant) -> list[tuple[str, str]]:
63-
"""Return entity_ids and friendly names for all todo lists of the domain Local Todo."""
64-
registry = entity_registry.async_get(hass)
65-
return [(entry.entity_id, hass.states.get(entry.entity_id).attributes.get("friendly_name", entry.entity_id)) for
66-
entry in registry.entities.values() if
67-
entry.platform == LOCAL_TODO_DOMAIN and entry.entity_id.startswith("todo.")]
68-
69-
7071
async def validate_options(user_input: dict[str, Any]) -> dict[str, Any]:
7172
if user_input.get(CONF_ACTIVE) is None:
7273
user_input[CONF_ACTIVE] = True
@@ -97,11 +98,14 @@ async def validate_options(user_input: dict[str, Any]) -> dict[str, Any]:
9798

9899
return {
99100
CONF_ACTIVE: user_input[CONF_ACTIVE],
101+
CONF_ACTIVE_OVERRIDE: user_input.get(CONF_ACTIVE_OVERRIDE) or None,
100102
CONF_TASK_INTERVAL_VALUE: user_input[CONF_TASK_INTERVAL_VALUE],
101103
CONF_TASK_INTERVAL_TYPE: user_input[CONF_TASK_INTERVAL_TYPE],
104+
CONF_TASK_INTERVAL_OVERRIDE: user_input.get(CONF_TASK_INTERVAL_OVERRIDE) or None,
102105
CONF_ICON: user_input[CONF_ICON],
103106
CONF_TAGS: user_input[CONF_TAGS],
104107
CONF_TODO_LISTS: user_input[CONF_TODO_LISTS],
105108
CONF_TODO_OFFSET_DAYS: user_input[CONF_TODO_OFFSET_DAYS],
109+
CONF_TODO_OFFSET_OVERRIDE: user_input.get(CONF_TODO_OFFSET_OVERRIDE) or None,
106110
CONF_NOTIFICATION_INTERVAL: user_input[CONF_NOTIFICATION_INTERVAL],
107111
}

sensor.py

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from homeassistant.util.dt import UTC
2525
from .const import DOMAIN, CONF_TASK_INTERVAL_VALUE, CONF_NOTIFICATION_INTERVAL, CONF_TAGS, CONF_ACTIVE, CONF_WEEK, \
2626
CONF_MONTH, CONF_YEAR, CONST_DUE, CONST_INACTIVE, CONST_DONE, CONF_TASK_INTERVAL_TYPE, CONF_TODO_OFFSET_DAYS, \
27-
CONF_TODO_LISTS, CONF_DAY
27+
CONF_TODO_LISTS, CONF_DAY, CONF_ACTIVE_OVERRIDE, CONF_TASK_INTERVAL_OVERRIDE, CONF_TODO_OFFSET_OVERRIDE
2828

2929
LOGGER = getLogger(__name__)
3030

@@ -41,7 +41,10 @@ async def async_setup_entry(
4141
[TaskTrackerSensor(data[CONF_NAME], options[CONF_TASK_INTERVAL_VALUE], options[CONF_TASK_INTERVAL_TYPE],
4242
options[CONF_NOTIFICATION_INTERVAL], options[CONF_TODO_LISTS],
4343
options[CONF_TODO_OFFSET_DAYS], options[CONF_TAGS],
44-
options[CONF_ACTIVE], options[CONF_ICON], entry.entry_id, hass)])
44+
options[CONF_ACTIVE], options[CONF_ICON], entry.entry_id, hass,
45+
options.get(CONF_ACTIVE_OVERRIDE),
46+
options.get(CONF_TASK_INTERVAL_OVERRIDE),
47+
options.get(CONF_TODO_OFFSET_OVERRIDE))])
4548

4649

4750
class TaskTrackerSensor(RestoreSensor, SensorEntity):
@@ -53,7 +56,10 @@ class TaskTrackerSensor(RestoreSensor, SensorEntity):
5356

5457
def __init__(self, entry_name: str, task_interval_value: int, task_interval_type: str,
5558
notification_interval: int, todo_lists: list[str], todo_offset_days: int, tags: str, active: bool,
56-
icon: str, entry_id: str, hass: HomeAssistant) -> None:
59+
icon: str, entry_id: str, hass: HomeAssistant,
60+
active_override: str | None = None,
61+
task_interval_override: str | None = None,
62+
todo_offset_override: str | None = None) -> None:
5763
"""Initialize the sensor with a service name."""
5864
self.task_interval_value: int = task_interval_value
5965
self.task_interval_type: str = task_interval_type
@@ -70,6 +76,12 @@ def __init__(self, entry_name: str, task_interval_value: int, task_interval_type
7076
self.due_date: date = date(1970, 1, 1)
7177
self.due_in: int = 0
7278
self.mark_as_done_scheduled: Callable[[], None] | None = None
79+
self.active_override: str | None = active_override
80+
self.task_interval_override: str | None = task_interval_override
81+
self.todo_offset_override: str | None = todo_offset_override
82+
# Effective values after applying overrides; initialised to configured values
83+
self._effective_active: bool = active
84+
self._effective_todo_offset_days: int = todo_offset_days
7385

7486
device_id = f"{DOMAIN}_{self.entry_id}"
7587
self._attr_name = None
@@ -107,6 +119,16 @@ async def async_added_to_hass(self) -> None:
107119
)
108120
)
109121

122+
# Subscribe to override entity state changes
123+
if any([self.active_override, self.task_interval_override, self.todo_offset_override]):
124+
self.async_on_remove(
125+
self.hass.bus.async_listen(
126+
EVENT_STATE_CHANGED,
127+
self.async_update,
128+
self._filter_override_changes,
129+
)
130+
)
131+
110132
if self.hass.state == CoreState.running:
111133
await self.async_update()
112134
else:
@@ -118,34 +140,63 @@ async def async_added_to_hass(self) -> None:
118140
async def async_update(self, _=None) -> None:
119141
self._attr_native_value = CONST_DONE
120142

121-
if self.task_interval_type == CONF_WEEK:
122-
self.due_date = self.last_done + relativedelta(weeks=self.task_interval_value)
123-
elif self.task_interval_type == CONF_MONTH:
124-
self.due_date = self.last_done + relativedelta(months=self.task_interval_value)
125-
elif self.task_interval_type == CONF_YEAR:
126-
self.due_date = self.last_done + relativedelta(years=self.task_interval_value)
143+
effective_active = self.active
144+
if self.active_override:
145+
override_state = self.hass.states.get(self.active_override)
146+
if override_state is not None and override_state.state not in ("unavailable", "unknown"):
147+
effective_active = override_state.state == "on"
148+
149+
effective_task_interval_value = self.task_interval_value
150+
effective_task_interval_type = self.task_interval_type
151+
if self.task_interval_override:
152+
override_state = self.hass.states.get(self.task_interval_override)
153+
if override_state is not None and override_state.state not in ("unavailable", "unknown"):
154+
try:
155+
effective_task_interval_value = max(1, int(float(override_state.state)))
156+
effective_task_interval_type = CONF_DAY
157+
except (ValueError, TypeError):
158+
pass
159+
160+
effective_todo_offset_days = self.todo_offset_days
161+
if self.todo_offset_override:
162+
override_state = self.hass.states.get(self.todo_offset_override)
163+
if override_state is not None and override_state.state not in ("unavailable", "unknown"):
164+
try:
165+
effective_todo_offset_days = max(0, int(float(override_state.state)))
166+
except (ValueError, TypeError):
167+
pass
168+
169+
if effective_task_interval_type == CONF_WEEK:
170+
self.due_date = self.last_done + relativedelta(weeks=effective_task_interval_value)
171+
elif effective_task_interval_type == CONF_MONTH:
172+
self.due_date = self.last_done + relativedelta(months=effective_task_interval_value)
173+
elif effective_task_interval_type == CONF_YEAR:
174+
self.due_date = self.last_done + relativedelta(years=effective_task_interval_value)
127175
else:
128-
self.due_date = self.last_done + relativedelta(days=self.task_interval_value)
176+
self.due_date = self.last_done + relativedelta(days=effective_task_interval_value)
129177

130178
self.due_in: int = (self.due_date - date.today()).days if self.due_date > date.today() else 0
131179
overdue_by: int = (date.today() - self.due_date).days if self.due_date < date.today() else 0
132180

133-
if not self.active:
181+
if not effective_active:
134182
self._attr_native_value = CONST_INACTIVE
135183
elif self.due_in == 0:
136184
self._attr_native_value = CONST_DUE
137185

186+
self._effective_active: bool = effective_active
187+
self._effective_todo_offset_days: int = effective_todo_offset_days
188+
138189
self._attr_extra_state_attributes: dict[str, str | int | list] = {
139190
"last_done": str(self.last_done),
140191
"due_date": str(self.due_date),
141192
"due_in": self.due_in,
142193
"overdue_by": overdue_by,
143-
"task_interval_value": self.task_interval_value,
144-
"task_interval_type": self.task_interval_type,
194+
"task_interval_value": effective_task_interval_value,
195+
"task_interval_type": effective_task_interval_type,
145196
"icon": self.icon,
146197
"tags": self.tags,
147198
"todo_lists": self.todo_lists,
148-
"todo_offset_days": self.todo_offset_days,
199+
"todo_offset_days": effective_todo_offset_days,
149200
"notification_interval": self.notification_interval,
150201
}
151202
self.async_write_ha_state()
@@ -159,6 +210,13 @@ def _filter_state_changes(self, event_data: EventStateChangedData) -> bool:
159210
return event_data["entity_id"] in self.todo_lists and event_data.get("old_state") and event_data.get(
160211
"new_state") and event_data["new_state"].state < event_data["old_state"].state
161212

213+
@callback
214+
def _filter_override_changes(self, event_data: EventStateChangedData) -> bool:
215+
"""Listen only for state changes in override entities."""
216+
override_entities = {e for e in
217+
[self.active_override, self.task_interval_override, self.todo_offset_override] if e}
218+
return event_data["entity_id"] in override_entities
219+
162220
async def async_todo_list_changed(self, event: Any) -> None:
163221
if self.mark_as_done_scheduled:
164222
self.mark_as_done_scheduled()
@@ -185,7 +243,7 @@ async def async_todo_list_changed_deferred(self, event: Any, _) -> None:
185243
async def async_sync_todo_list(self, todo_list: str) -> None:
186244
existing_item: dict | None = await self.async_get_item_from_todo_list(todo_list)
187245

188-
if self.active and self.due_in <= self.todo_offset_days:
246+
if self._effective_active and self.due_in <= self._effective_todo_offset_days:
189247
# there is supposed to be an item in the todo list
190248
if existing_item is None:
191249
# The item does not exist, so we need to add it

strings.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,28 @@
3636
"data": {
3737
"task_interval_value": "Task Interval",
3838
"task_interval_type": "Task Interval Unit",
39+
"task_interval_override": "Task Interval Override",
3940
"notification_interval": "Notification Interval (days)",
4041
"todo_lists": "Todo-Lists",
4142
"todo_offset_days": "Todo-List Offset (days)",
43+
"todo_offset_override": "Todo-List Offset Override (days)",
4244
"tags": "Tags",
4345
"icon": "Icon",
44-
"active": "Active"
46+
"active": "Active",
47+
"active_override": "Active Override"
4548
},
4649
"data_description": {
4750
"task_interval_value": "Time after which the task becomes due again.",
4851
"task_interval_type": "The unit of time for the task interval.",
52+
"task_interval_override": "Select an input_number (value in days) to override the Task Interval.",
4953
"notification_interval": "The time in days after which notifications are sent again.",
5054
"todo_lists": "Todo-Lists, to which this task will be added.",
5155
"todo_offset_days": "The number of days before the task is due, to add the task to the Todo-List(s).",
56+
"todo_offset_override": "Select an input_number (value in days) to override the Todo-List Offset.",
5257
"tags": "Tags to categorize the task, separated by commas.",
5358
"icon": "Icon to represent the task.",
54-
"active": "Enable or disable the task."
59+
"active": "Enable or disable the task.",
60+
"active_override": "Select an input_boolean to override the Active setting."
5561
}
5662
}
5763
}

0 commit comments

Comments
 (0)