diff --git a/custom_components/chore_helper/config_flow.py b/custom_components/chore_helper/config_flow.py index 7e972aa..3699fe7 100644 --- a/custom_components/chore_helper/config_flow.py +++ b/custom_components/chore_helper/config_flow.py @@ -58,57 +58,59 @@ def optional( return vol.Optional(key, description={"suggested_value": suggested_value}) +def general_schema_definition( + handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, +) -> Mapping[str, Any]: + """Create general schema.""" + schema = { + required( + const.CONF_FREQUENCY, handler.options, const.DEFAULT_FREQUENCY + ): selector.SelectSelector( + selector.SelectSelectorConfig(options=const.FREQUENCY_OPTIONS) + ), + optional( + const.CONF_ICON_NORMAL, handler.options, const.DEFAULT_ICON_NORMAL + ): selector.IconSelector(), + optional( + const.CONF_ICON_TOMORROW, handler.options, const.DEFAULT_ICON_TOMORROW + ): selector.IconSelector(), + optional( + const.CONF_ICON_TODAY, handler.options, const.DEFAULT_ICON_TODAY + ): selector.IconSelector(), + optional( + const.CONF_ICON_OVERDUE, handler.options, const.DEFAULT_ICON_OVERDUE + ): selector.IconSelector(), + optional( + const.CONF_FORECAST_DATES, handler.options, const.DEFAULT_FORECAST_DATES + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=100, + mode=selector.NumberSelectorMode.BOX, + step=1, + ) + ), + optional(ATTR_HIDDEN, handler.options, False): bool, + optional(const.CONF_MANUAL, handler.options, False): bool, + } + + return schema + + async def general_config_schema( handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, ) -> vol.Schema: """Generate config schema.""" - return vol.Schema( - { - optional(CONF_NAME, handler.options): selector.TextSelector(), - required( - const.CONF_FREQUENCY, handler.options, const.DEFAULT_FREQUENCY - ): selector.SelectSelector( - selector.SelectSelectorConfig(options=const.FREQUENCY_OPTIONS) - ), - optional( - const.CONF_ICON_NORMAL, handler.options, const.DEFAULT_ICON_NORMAL - ): selector.IconSelector(), - optional( - const.CONF_ICON_TODAY, handler.options, const.DEFAULT_ICON_TODAY - ): selector.IconSelector(), - optional( - const.CONF_ICON_TOMORROW, handler.options, const.DEFAULT_ICON_TOMORROW - ): selector.IconSelector(), - optional(ATTR_HIDDEN, handler.options, False): bool, - optional(const.CONF_MANUAL, handler.options, False): bool, - } - ) + schema_obj = {required(CONF_NAME, handler.options): selector.TextSelector()} + schema_obj.update(general_schema_definition(handler)) + return vol.Schema(schema_obj) async def general_options_schema( handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler, ) -> vol.Schema: """Generate options schema.""" - return vol.Schema( - { - required( - const.CONF_FREQUENCY, handler.options, const.DEFAULT_FREQUENCY - ): selector.SelectSelector( - selector.SelectSelectorConfig(options=const.FREQUENCY_OPTIONS) - ), - optional( - const.CONF_ICON_NORMAL, handler.options, const.DEFAULT_ICON_NORMAL - ): selector.IconSelector(), - optional( - const.CONF_ICON_TODAY, handler.options, const.DEFAULT_ICON_TODAY - ): selector.IconSelector(), - optional( - const.CONF_ICON_TOMORROW, handler.options, const.DEFAULT_ICON_TOMORROW - ): selector.IconSelector(), - optional(ATTR_HIDDEN, handler.options, False): bool, - optional(const.CONF_MANUAL, handler.options, False): bool, - } - ) + return vol.Schema(general_schema_definition(handler)) async def detail_config_schema( diff --git a/custom_components/chore_helper/const.py b/custom_components/chore_helper/const.py index 12e4ede..beaff63 100644 --- a/custom_components/chore_helper/const.py +++ b/custom_components/chore_helper/const.py @@ -27,11 +27,13 @@ CONF_SENSOR = "sensor" CONF_ENABLED = "enabled" +CONF_FORECAST_DATES = "forecast_dates" CONF_FREQUENCY = "frequency" CONF_MANUAL = "manual_update" CONF_ICON_NORMAL = "icon_normal" CONF_ICON_TODAY = "icon_today" CONF_ICON_TOMORROW = "icon_tomorrow" +CONF_ICON_OVERDUE = "icon_overdue" CONF_OFFSET = "offset" CONF_DAY_OF_MONTH = "day_of_month" CONF_FIRST_MONTH = "first_month" @@ -54,10 +56,12 @@ DEFAULT_PERIOD = 1 DEFAULT_FIRST_WEEK = 1 DEFAULT_DATE_FORMAT = "%b-%d-%Y" +DEFAULT_FORECAST_DATES = 10 DEFAULT_ICON_NORMAL = "mdi:broom" DEFAULT_ICON_TODAY = "mdi:bell" DEFAULT_ICON_TOMORROW = "mdi:bell-outline" +DEFAULT_ICON_OVERDUE = "mdi:bell-alert" ICON = DEFAULT_ICON_NORMAL STATE_TODAY = "today" diff --git a/custom_components/chore_helper/sensor.py b/custom_components/chore_helper/sensor.py index b0a62b0..4b9318d 100644 --- a/custom_components/chore_helper/sensor.py +++ b/custom_components/chore_helper/sensor.py @@ -5,6 +5,7 @@ from datetime import date, datetime, time, timedelta from typing import Any from collections.abc import Generator +import itertools from dateutil.relativedelta import relativedelta from homeassistant.config_entries import ConfigEntry @@ -71,10 +72,12 @@ class Chore(RestoreEntity): "_icon_normal", "_icon_today", "_icon_tomorrow", + "_icon_overdue", "_last_month", "_last_updated", "_manual", "_next_due_date", + "_forecast_dates", "_overdue", "_overdue_days", "_frequency", @@ -109,9 +112,11 @@ def __init__(self, config_entry: ConfigEntry) -> None: self._icon_normal = config.get(const.CONF_ICON_NORMAL) self._icon_today = config.get(const.CONF_ICON_TODAY) self._icon_tomorrow = config.get(const.CONF_ICON_TOMORROW) + self._icon_overdue = config.get(const.CONF_ICON_OVERDUE) self._date_format = config.get( const.CONF_DATE_FORMAT, const.DEFAULT_DATE_FORMAT ) + self._forecast_dates: int = config.get(const.CONF_FORECAST_DATES) or 0 self._due_dates: list[date] = [] self._next_due_date: date | None = None self._last_updated: datetime | None = None @@ -395,7 +400,9 @@ def chore_schedule(self) -> Generator[date, None, None]: today = helpers.now().date() start_date: date = self._calculate_start_date() last_date: date = date(today.year + 1, 12, 31) - while True: + for i in itertools.count(): + if i > self._forecast_dates: + break try: next_due_date = self._find_candidate_date(start_date) except (TypeError, ValueError): @@ -556,11 +563,12 @@ def update_state(self) -> None: self._attr_state = self._days if self._days > 1: self._attr_icon = self._icon_normal - else: - if self._days == 0: - self._attr_icon = self._icon_today - elif self._days == 1: - self._attr_icon = self._icon_tomorrow + elif self._days < 0: + self._attr_icon = self._icon_overdue + elif self._days == 0: + self._attr_icon = self._icon_today + elif self._days == 1: + self._attr_icon = self._icon_tomorrow self._overdue = self._days < 0 self._overdue_days = 0 if self._days > -1 else abs(self._days) else: diff --git a/custom_components/chore_helper/translations/en.json b/custom_components/chore_helper/translations/en.json index c427c3d..33a2796 100644 --- a/custom_components/chore_helper/translations/en.json +++ b/custom_components/chore_helper/translations/en.json @@ -11,7 +11,9 @@ "manual_update": "Manual update - sensor state updated manually by a service (Blueprint)", "icon_normal": "Icon (mdi:broom) - optional", "icon_tomorrow": "Icon due tomorrow (mdi:bell-outline) - optional", - "icon_today": "Icon due today (mdi:bell) - optional" + "icon_today": "Icon due today (mdi:bell) - optional", + "icon_overdue": "Icon overdue (mdi:bell-alert) - optional", + "forecast_dates": "Number of future due dates to forecast" } }, "detail": { @@ -61,7 +63,9 @@ "manual_update": "Manual update - sensor state updated manually by a service (Blueprint)", "icon_normal": "Icon (mdi:broom) - optional", "icon_tomorrow": "Icon due tomorrow (mdi:bell-outline) - optional", - "icon_today": "Icon due today (mdi:bell) - optional" + "icon_today": "Icon due today (mdi:bell) - optional", + "icon_overdue": "Icon overdue (mdi:bell-alert) - optional", + "forecast_dates": "Number of future due dates to forecast" } }, "detail": {