Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions custom_components/taskmate/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Constants for TaskMate integration."""

import re
from typing import Final

DOMAIN: Final = "taskmate"
Expand Down Expand Up @@ -76,6 +77,28 @@
"every_6_months": 182,
}

# Custom "every N days" recurrences (#1038) are stored as ``every_<N>_days``;
# the original every_2_days is simply the N=2 case. N is bounded so a typo
# can't park a chore for years.
MIN_RECURRENCE_INTERVAL_DAYS: Final = 2
MAX_RECURRENCE_INTERVAL_DAYS: Final = 365
_EVERY_N_DAYS_RE: Final = re.compile(r"every_(\d{1,3})_days")


def recurrence_interval_days(recurrence: str | None) -> int | None:
"""N for an ``every_<N>_days`` recurrence, else None (also when N is out of range)."""
match = _EVERY_N_DAYS_RE.fullmatch(recurrence) if isinstance(recurrence, str) else None
if not match:
return None
days = int(match.group(1))
return days if MIN_RECURRENCE_INTERVAL_DAYS <= days <= MAX_RECURRENCE_INTERVAL_DAYS else None


def recurrence_period_days(recurrence: str | None) -> int:
"""Nominal period of a Mode-B recurrence in days (weekly when unknown)."""
return recurrence_interval_days(recurrence) or RECURRENCE_PERIOD_DAYS.get(recurrence or "", 7)


# Badge tiers, lowest to highest.
BADGE_TIERS: Final = ["bronze", "silver", "gold", "platinum"]

Expand Down
6 changes: 4 additions & 2 deletions custom_components/taskmate/coord_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
MAX_CALENDAR_PROJECTION_DAYS,
MIN_CALENDAR_PROJECTION_DAYS,
TIME_CATEGORY_ICONS,
recurrence_interval_days,
)
from .models import Chore

Expand Down Expand Up @@ -245,11 +246,12 @@ def _is_chore_base_scheduled_for_date(self, chore: Chore, day: date) -> bool:
pass
return True

if recurrence == "every_2_days" and anchor_iso:
interval_days = recurrence_interval_days(recurrence)
if interval_days and anchor_iso:
try:
anchor = date.fromisoformat(anchor_iso)
diff = (day - anchor).days
return diff >= 0 and diff % 2 == 0
return diff >= 0 and diff % interval_days == 0
except ValueError:
return False

Expand Down
21 changes: 12 additions & 9 deletions custom_components/taskmate/coord_chores.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@

from . import images, photos
from .chore_undo import child_can_undo, undo_window_seconds
from .const import CHORE_NOTE_MAX_LENGTH, CHORE_SUGGESTED_POINTS_MAX, DAILY_PROGRESS_KEEP_DAYS, QUALITY_RATINGS
from .const import (
CHORE_NOTE_MAX_LENGTH,
CHORE_SUGGESTED_POINTS_MAX,
DAILY_PROGRESS_KEEP_DAYS,
QUALITY_RATINGS,
recurrence_interval_days,
)
from .coord_rejections import clean_reject_reason
from .coord_teamwork import teamwork_config_error
from .models import Chore, ChoreCompletion, PointsTransaction
Expand Down Expand Up @@ -1823,11 +1829,8 @@ def is_chore_available_for_child(self, chore, child_id: str) -> bool:
now = dt_util.now()
today = dt_util.as_local(now).date()

window_days = {
"every_2_days": 2,
"weekly": 7,
"every_2_weeks": 14,
}.get(recurrence, 7)
interval_days = recurrence_interval_days(recurrence)
window_days = interval_days or {"weekly": 7, "every_2_weeks": 14}.get(recurrence, 7)

record = self.storage.get_last_completed(chore.id, child_id)
current_iso = record.get("current")
Expand Down Expand Up @@ -1863,14 +1866,14 @@ def is_chore_available_for_child(self, chore, child_id: str) -> bool:
# from, so moving one week's chore doesn't shift every week after it.
last_dt = self.occurrence_origin(chore, last_dt)

# every_2_days with anchor — check alignment
if recurrence == "every_2_days" and recurrence_start:
# every_<N>_days with anchor — check alignment
if interval_days and recurrence_start:
try:
anchor = date.fromisoformat(recurrence_start)
days_since_anchor = (today - anchor).days
if days_since_anchor < 0:
return False
if days_since_anchor % 2 != 0:
if days_since_anchor % interval_days != 0:
return False
return last_dt < today
except ValueError:
Expand Down
6 changes: 3 additions & 3 deletions custom_components/taskmate/coord_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from homeassistant.util import dt as dt_util

from .const import RECURRENCE_PERIOD_DAYS
from .const import recurrence_period_days

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -175,7 +175,7 @@ def _expected_occurrences(self, chore, start: date, end: date) -> int:
shift += 1

if mode == "recurring":
period_days = RECURRENCE_PERIOD_DAYS.get(getattr(chore, "recurrence", "weekly"), 7)
period_days = recurrence_period_days(getattr(chore, "recurrence", "weekly"))
return max(0, span // period_days + shift)

due_days = [d.lower() for d in (getattr(chore, "due_days", []) or [])]
Expand Down Expand Up @@ -340,7 +340,7 @@ def _chore_falls_on(self, chore, day: date) -> bool:
return override

if mode == "recurring":
period_days = RECURRENCE_PERIOD_DAYS.get(getattr(chore, "recurrence", "weekly"), 7)
period_days = recurrence_period_days(getattr(chore, "recurrence", "weekly"))
anchor_raw = getattr(chore, "recurrence_start", "") or ""
try:
anchor = date.fromisoformat(anchor_raw) if anchor_raw else None
Expand Down
6 changes: 4 additions & 2 deletions custom_components/taskmate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,11 @@ class Chore:
schedule_mode: str = "specific_days"
due_days: list[str] = field(default_factory=list) # Mode A: days to show chore
# Mode B fields
recurrence: str = "weekly" # every_2_days | weekly | every_2_weeks | monthly | every_3_months | every_6_months
recurrence: str = (
"weekly" # every_<N>_days (N 2-365) | weekly | every_2_weeks | monthly | every_3_months | every_6_months
)
recurrence_day: str = "" # optional: which day of week for weekly/every_2_weeks
recurrence_start: str = "" # optional: ISO date anchor for every_2_days
recurrence_start: str = "" # optional: ISO date anchor for every_<N>_days
first_occurrence_mode: str = "available_immediately" # available_immediately | wait_for_first_occurrence
# Dynamic visibility
visibility_entity: str = "" # optional: entity_id to check for visibility
Expand Down
17 changes: 15 additions & 2 deletions custom_components/taskmate/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@
DIFFICULTY_TIERS,
DOMAIN,
MAX_CHORE_TAGS,
MAX_RECURRENCE_INTERVAL_DAYS,
MAX_TIME_PERIODS,
MIN_RECURRENCE_INTERVAL_DAYS,
SCHEDULE_MODES,
TAG_ID_MAX_LENGTH,
TEAM_POINTS_MODES,
TEAM_SIZE_MAX,
TIME_CATEGORY_ICONS,
is_valid_completion_sound,
recurrence_interval_days,
)
from .coord_birthdays import normalize_birthday
from .coord_inspections import INSPECTION_BONUS_MAX, INSPECTION_FAIL_MODES, INSPECTION_WINDOWS
Expand Down Expand Up @@ -718,6 +721,16 @@ def _image_url_or_blank(value):
}


def _recurrence(value):
"""A recurrence name; a custom every_<N>_days interval (#1038) must have N in range."""
text = str(value)
if text.startswith("every_") and text.endswith("_days") and recurrence_interval_days(text) is None:
raise vol.Invalid(
f"Custom recurrence must be every {MIN_RECURRENCE_INTERVAL_DAYS}-{MAX_RECURRENCE_INTERVAL_DAYS} days"
)
return text


def _tag_id_list(value):
"""Validate the NFC / QR tag ids linked to a chore (#923).

Expand Down Expand Up @@ -755,7 +768,7 @@ def _chore_payload_schema(*, require_name: bool):
vol.Optional("difficulty"): vol.In(DIFFICULTY_TIERS),
vol.Optional("schedule_mode"): vol.In(SCHEDULE_MODES),
vol.Optional("due_days"): [str],
vol.Optional("recurrence"): str,
vol.Optional("recurrence"): _recurrence,
vol.Optional("recurrence_day"): str,
vol.Optional("recurrence_start"): str,
vol.Optional("first_occurrence_mode"): str,
Expand Down Expand Up @@ -2500,7 +2513,7 @@ async def _ws_templates_get(hass, connection, msg, coordinator):
vol.Optional("completion_sound"): _completion_sound,
vol.Optional("schedule_mode"): vol.In(SCHEDULE_MODES),
vol.Optional("due_days"): [str],
vol.Optional("recurrence"): str,
vol.Optional("recurrence"): _recurrence,
vol.Optional("recurrence_day"): str,
vol.Optional("recurrence_start"): str,
vol.Optional("first_occurrence_mode"): str,
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@
"panel.chore_points_label": "Point",
"panel.chore_points_per_window_label": "Point pr. periode",
"panel.chore_recurrence_day_hint": "f.eks. monday — lad feltet være tomt for en hvilken som helst dag",
"panel.chore_recurrence_interval_label": "Gentag hver (dage)",
"panel.chore_recurrence_interval_hint": "Helt antal dage, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Angiv et antal dage mellem {min} og {max}",
"panel.chore_recurrence_day_label": "Ugedag (kun ugentligt)",
"panel.chore_recurrence_label": "Gentagelse",
"panel.chore_recurrence_start_hint": "Startdato — tom = i dag",
Expand Down Expand Up @@ -1281,6 +1284,8 @@
"panel.pending_tooltip": "{count} afventer — klik for at gennemgå",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Hver 2. dag",
"panel.recurrence_every_n_days": "Hver N. dag (brugerdefineret)",
"panel.recurrence_every_count_days": "Hver {count}. dag",
"panel.recurrence_every_2_weeks": "Hver 2. uge",
"panel.recurrence_every_3_months": "Hver 3. måned",
"panel.recurrence_every_6_months": "Hver 6. måned",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,9 @@
"panel.chore_points_label": "Punkte",
"panel.chore_points_per_window_label": "Punkte pro Zeitfenster",
"panel.chore_recurrence_day_hint": "z.B. Montag — leer lassen für beliebigen Tag",
"panel.chore_recurrence_interval_label": "Wiederholen alle (Tage)",
"panel.chore_recurrence_interval_hint": "Ganze Anzahl Tage, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Gib eine Anzahl Tage zwischen {min} und {max} ein",
"panel.chore_recurrence_day_label": "Wochentag (nur wöchentlich)",
"panel.chore_recurrence_label": "Wiederholung",
"panel.chore_recurrence_start_hint": "Ankerdatum — leer = heute",
Expand Down Expand Up @@ -1231,6 +1234,8 @@
"panel.pending_tooltip": "{count} ausstehend — klicken zum Überprüfen",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Alle 2 Tage",
"panel.recurrence_every_n_days": "Alle N Tage (benutzerdefiniert)",
"panel.recurrence_every_count_days": "Alle {count} Tage",
"panel.recurrence_every_2_weeks": "Alle 2 Wochen",
"panel.recurrence_every_3_months": "Alle 3 Monate",
"panel.recurrence_every_6_months": "Alle 6 Monate",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@
"panel.chore_points_label": "Points",
"panel.chore_points_per_window_label": "Points per window",
"panel.chore_recurrence_day_hint": "e.g. monday — leave blank for any day",
"panel.chore_recurrence_interval_label": "Repeat every (days)",
"panel.chore_recurrence_interval_hint": "Whole number of days, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Enter a number of days between {min} and {max}",
"panel.chore_recurrence_day_label": "Day of week (weekly only)",
"panel.chore_recurrence_label": "Recurrence",
"panel.chore_recurrence_start_hint": "Anchor date — blank = today",
Expand Down Expand Up @@ -1281,6 +1284,8 @@
"panel.pending_tooltip": "{count} pending — click to review",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Every 2 days",
"panel.recurrence_every_n_days": "Every N days (custom)",
"panel.recurrence_every_count_days": "Every {count} days",
"panel.recurrence_every_2_weeks": "Every 2 weeks",
"panel.recurrence_every_3_months": "Every 3 months",
"panel.recurrence_every_6_months": "Every 6 months",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@
"panel.chore_points_label": "Points",
"panel.chore_points_per_window_label": "Points per window",
"panel.chore_recurrence_day_hint": "e.g. monday — leave blank for any day",
"panel.chore_recurrence_interval_label": "Repeat every (days)",
"panel.chore_recurrence_interval_hint": "Whole number of days, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Enter a number of days between {min} and {max}",
"panel.chore_recurrence_day_label": "Day of week (weekly only)",
"panel.chore_recurrence_label": "Recurrence",
"panel.chore_recurrence_start_hint": "Anchor date — blank = today",
Expand Down Expand Up @@ -1281,6 +1284,8 @@
"panel.pending_tooltip": "{count} pending — click to review",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Every 2 days",
"panel.recurrence_every_n_days": "Every N days (custom)",
"panel.recurrence_every_count_days": "Every {count} days",
"panel.recurrence_every_2_weeks": "Every 2 weeks",
"panel.recurrence_every_3_months": "Every 3 months",
"panel.recurrence_every_6_months": "Every 6 months",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,9 @@
"panel.chore_points_label": "Points",
"panel.chore_points_per_window_label": "Points par période",
"panel.chore_recurrence_day_hint": "ex. lundi — laisser vide pour n'importe quel jour",
"panel.chore_recurrence_interval_label": "Répéter tous les (jours)",
"panel.chore_recurrence_interval_hint": "Nombre entier de jours, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Saisissez un nombre de jours entre {min} et {max}",
"panel.chore_recurrence_day_label": "Jour de la semaine (hebdomadaire uniquement)",
"panel.chore_recurrence_label": "Récurrence",
"panel.chore_recurrence_start_hint": "Date de début — vide = aujourd'hui",
Expand Down Expand Up @@ -1231,6 +1234,8 @@
"panel.pending_tooltip": "{count} en attente — cliquer pour vérifier",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Tous les 2 jours",
"panel.recurrence_every_n_days": "Tous les N jours (personnalisé)",
"panel.recurrence_every_count_days": "Tous les {count} jours",
"panel.recurrence_every_2_weeks": "Toutes les 2 semaines",
"panel.recurrence_every_3_months": "Tous les 3 mois",
"panel.recurrence_every_6_months": "Tous les 6 mois",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,9 @@
"panel.chore_points_label": "Poeng",
"panel.chore_points_per_window_label": "Poeng per vindu",
"panel.chore_recurrence_day_hint": "f.eks. mandag — la stå tomt for hvilken som helst dag",
"panel.chore_recurrence_interval_label": "Gjenta hver (dager)",
"panel.chore_recurrence_interval_hint": "Helt antall dager, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Skriv inn et antall dager mellom {min} og {max}",
"panel.chore_recurrence_day_label": "Ukedag (kun ukentlig)",
"panel.chore_recurrence_label": "Gjentakelse",
"panel.chore_recurrence_start_hint": "Ankerdato — tomt = i dag",
Expand Down Expand Up @@ -1231,6 +1234,8 @@
"panel.pending_tooltip": "{count} ventende — klikk for å gjennomgå",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Annenhver dag",
"panel.recurrence_every_n_days": "Hver N. dag (egendefinert)",
"panel.recurrence_every_count_days": "Hver {count}. dag",
"panel.recurrence_every_2_weeks": "Annenhver uke",
"panel.recurrence_every_3_months": "Hver 3. måned",
"panel.recurrence_every_6_months": "Hvert halvår",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/nn.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,9 @@
"panel.chore_points_label": "Poeng",
"panel.chore_points_per_window_label": "Poeng per vindauge",
"panel.chore_recurrence_day_hint": "t.d. måndag — la stå tomt for kva dag som helst",
"panel.chore_recurrence_interval_label": "Gjenta kvar (dagar)",
"panel.chore_recurrence_interval_hint": "Heilt tal dagar, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Skriv inn eit tal dagar mellom {min} og {max}",
"panel.chore_recurrence_day_label": "Vekedag (berre vekentleg)",
"panel.chore_recurrence_label": "Gjentaking",
"panel.chore_recurrence_start_hint": "Ankerdato — tomt = i dag",
Expand Down Expand Up @@ -1231,6 +1234,8 @@
"panel.pending_tooltip": "{count} ventande — klikk for å gjennomgå",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Kvar 2. dag",
"panel.recurrence_every_n_days": "Kvar N. dag (eigendefinert)",
"panel.recurrence_every_count_days": "Kvar {count}. dag",
"panel.recurrence_every_2_weeks": "Kvar 2. veke",
"panel.recurrence_every_3_months": "Kvar 3. månad",
"panel.recurrence_every_6_months": "Kvar 6. månad",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@
"panel.chore_points_label": "Punkty",
"panel.chore_points_per_window_label": "Punkty za okres",
"panel.chore_recurrence_day_hint": "np. monday — pozostaw puste dla dowolnego dnia",
"panel.chore_recurrence_interval_label": "Powtarzaj co (dni)",
"panel.chore_recurrence_interval_hint": "Całkowita liczba dni, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Podaj liczbę dni od {min} do {max}",
"panel.chore_recurrence_day_label": "Dzień tygodnia (tylko co tydzień)",
"panel.chore_recurrence_label": "Powtarzanie",
"panel.chore_recurrence_start_hint": "Data początkowa — puste = dziś",
Expand Down Expand Up @@ -1281,6 +1284,8 @@
"panel.pending_tooltip": "Oczekujące: {count} — kliknij, aby przejrzeć",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "Co 2 dni",
"panel.recurrence_every_n_days": "Co N dni (własne)",
"panel.recurrence_every_count_days": "Co {count} dni",
"panel.recurrence_every_2_weeks": "Co 2 tygodnie",
"panel.recurrence_every_3_months": "Co 3 miesiące",
"panel.recurrence_every_6_months": "Co 6 miesięcy",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,9 @@
"panel.chore_points_label": "Pontos",
"panel.chore_points_per_window_label": "Pontos por janela",
"panel.chore_recurrence_day_hint": "ex: segunda — deixe em branco para qualquer dia",
"panel.chore_recurrence_interval_label": "Repetir a cada (dias)",
"panel.chore_recurrence_interval_hint": "Número inteiro de dias, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Informe um número de dias entre {min} e {max}",
"panel.chore_recurrence_day_label": "Dia da semana (somente semanal)",
"panel.chore_recurrence_label": "Recorrência",
"panel.chore_recurrence_start_hint": "Data âncora — em branco = hoje",
Expand Down Expand Up @@ -1231,6 +1234,8 @@
"panel.pending_tooltip": "{count} pendente(s) — clique para revisar",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "A cada 2 dias",
"panel.recurrence_every_n_days": "A cada N dias (personalizado)",
"panel.recurrence_every_count_days": "A cada {count} dias",
"panel.recurrence_every_2_weeks": "A cada 2 semanas",
"panel.recurrence_every_3_months": "A cada 3 meses",
"panel.recurrence_every_6_months": "A cada 6 meses",
Expand Down
5 changes: 5 additions & 0 deletions custom_components/taskmate/www/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,9 @@
"panel.chore_points_label": "Pontos",
"panel.chore_points_per_window_label": "Pontos por janela",
"panel.chore_recurrence_day_hint": "ex. segunda-feira — deixe em branco para qualquer dia",
"panel.chore_recurrence_interval_label": "Repetir a cada (dias)",
"panel.chore_recurrence_interval_hint": "Número inteiro de dias, {min}–{max}",
"panel.chore_recurrence_interval_invalid": "Introduza um número de dias entre {min} e {max}",
"panel.chore_recurrence_day_label": "Dia da semana (apenas semanal)",
"panel.chore_recurrence_label": "Recorrência",
"panel.chore_recurrence_start_hint": "Data âncora — em branco = hoje",
Expand Down Expand Up @@ -1236,6 +1239,8 @@
"panel.pending_tooltip": "{count} pendente(s) — clique para revisar",
"panel.pts_display": "{count} {points_name}",
"panel.recurrence_every_2_days": "A cada 2 dias",
"panel.recurrence_every_n_days": "A cada N dias (personalizado)",
"panel.recurrence_every_count_days": "A cada {count} dias",
"panel.recurrence_every_2_weeks": "A cada 2 semanas",
"panel.recurrence_every_3_months": "A cada 3 meses",
"panel.recurrence_every_6_months": "A cada 6 meses",
Expand Down
Loading
Loading