feat(tracking): per-config quiet hours with app-level IANA timezone
Add quiet_hours_enabled/start/end to TrackingConfig (HH:MM strings interpreted in the app-level timezone AppSetting). The dispatch path loads the app timezone once per run and passes it through event_allowed_by_config -> in_quiet_hours, so overnight windows like 22:00-07:00 work correctly in any IANA tz. Frontend exposes a Timezone field under Settings and a Quiet Hours section on the Immich tracking-config form with time-picker inputs.
This commit is contained in:
@@ -523,6 +523,9 @@
|
|||||||
"memorySource": "Memory source",
|
"memorySource": "Memory source",
|
||||||
"memorySourceAlbums": "Scan tracked albums",
|
"memorySourceAlbums": "Scan tracked albums",
|
||||||
"memorySourceNative": "Immich native memories",
|
"memorySourceNative": "Immich native memories",
|
||||||
|
"quietHours": "Quiet hours",
|
||||||
|
"quietHoursStart": "Start",
|
||||||
|
"quietHoursEnd": "End",
|
||||||
"test": "Test",
|
"test": "Test",
|
||||||
"confirmDelete": "Delete this tracking config?",
|
"confirmDelete": "Delete this tracking config?",
|
||||||
"sortNone": "None",
|
"sortNone": "None",
|
||||||
@@ -670,6 +673,8 @@
|
|||||||
"webhookSecretHint": "Secret token to verify webhook requests from Telegram",
|
"webhookSecretHint": "Secret token to verify webhook requests from Telegram",
|
||||||
"cacheTtl": "Media Cache TTL (hours)",
|
"cacheTtl": "Media Cache TTL (hours)",
|
||||||
"cacheTtlHint": "How long to cache uploaded Telegram file_ids before re-uploading",
|
"cacheTtlHint": "How long to cache uploaded Telegram file_ids before re-uploading",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"timezoneHint": "IANA timezone (e.g. UTC, Europe/Warsaw, America/New_York). Used to interpret HH:MM fields like quiet hours.",
|
||||||
"locales": "Template Languages",
|
"locales": "Template Languages",
|
||||||
"supportedLocales": "Supported Locales",
|
"supportedLocales": "Supported Locales",
|
||||||
"supportedLocalesHint": "Comma-separated locale codes for template editing (e.g. en,ru,de,fr)",
|
"supportedLocalesHint": "Comma-separated locale codes for template editing (e.g. en,ru,de,fr)",
|
||||||
@@ -680,6 +685,7 @@
|
|||||||
"scheduledAssets": "Sends random or selected photos from tracked albums on a schedule. Like a daily photo pick.",
|
"scheduledAssets": "Sends random or selected photos from tracked albums on a schedule. Like a daily photo pick.",
|
||||||
"memoryMode": "\"On This Day\" — sends photos taken on this date in previous years. Nostalgic flashbacks.",
|
"memoryMode": "\"On This Day\" — sends photos taken on this date in previous years. Nostalgic flashbacks.",
|
||||||
"memorySource": "Albums: scans tracked albums for date-matching assets. Native: uses Immich's built-in memories (covers entire library, optionally filtered by tracked albums).",
|
"memorySource": "Albums: scans tracked albums for date-matching assets. Native: uses Immich's built-in memories (covers entire library, optionally filtered by tracked albums).",
|
||||||
|
"quietHours": "Suppress all notifications during this HH:MM window (interpreted in the app timezone). Overnight windows like 22:00–07:00 are supported.",
|
||||||
"favoritesOnly": "Only include assets marked as favorites.",
|
"favoritesOnly": "Only include assets marked as favorites.",
|
||||||
"maxAssets": "Maximum number of asset details to include in a single notification message.",
|
"maxAssets": "Maximum number of asset details to include in a single notification message.",
|
||||||
"periodicStartDate": "The reference date for calculating periodic intervals. Summaries are sent every N days from this date.",
|
"periodicStartDate": "The reference date for calculating periodic intervals. Summaries are sent every N days from this date.",
|
||||||
|
|||||||
@@ -523,6 +523,9 @@
|
|||||||
"memorySource": "Источник воспоминаний",
|
"memorySource": "Источник воспоминаний",
|
||||||
"memorySourceAlbums": "Сканировать альбомы",
|
"memorySourceAlbums": "Сканировать альбомы",
|
||||||
"memorySourceNative": "Встроенные воспоминания Immich",
|
"memorySourceNative": "Встроенные воспоминания Immich",
|
||||||
|
"quietHours": "Тихие часы",
|
||||||
|
"quietHoursStart": "Начало",
|
||||||
|
"quietHoursEnd": "Конец",
|
||||||
"test": "Тест",
|
"test": "Тест",
|
||||||
"confirmDelete": "Удалить эту конфигурацию отслеживания?",
|
"confirmDelete": "Удалить эту конфигурацию отслеживания?",
|
||||||
"sortNone": "Нет",
|
"sortNone": "Нет",
|
||||||
@@ -670,6 +673,8 @@
|
|||||||
"webhookSecretHint": "Секретный токен для проверки запросов вебхука от Telegram",
|
"webhookSecretHint": "Секретный токен для проверки запросов вебхука от Telegram",
|
||||||
"cacheTtl": "TTL кэша медиа (часы)",
|
"cacheTtl": "TTL кэша медиа (часы)",
|
||||||
"cacheTtlHint": "Сколько хранить кэш Telegram file_id перед повторной загрузкой",
|
"cacheTtlHint": "Сколько хранить кэш Telegram file_id перед повторной загрузкой",
|
||||||
|
"timezone": "Часовой пояс",
|
||||||
|
"timezoneHint": "Часовой пояс IANA (например UTC, Europe/Warsaw, America/New_York). Используется для интерпретации полей HH:MM, таких как тихие часы.",
|
||||||
"locales": "Языки шаблонов",
|
"locales": "Языки шаблонов",
|
||||||
"supportedLocales": "Поддерживаемые локали",
|
"supportedLocales": "Поддерживаемые локали",
|
||||||
"supportedLocalesHint": "Коды локалей через запятую для редактирования шаблонов (например en,ru,de,fr)",
|
"supportedLocalesHint": "Коды локалей через запятую для редактирования шаблонов (например en,ru,de,fr)",
|
||||||
@@ -680,6 +685,7 @@
|
|||||||
"scheduledAssets": "Отправляет случайные или выбранные фото из альбомов по расписанию. Как ежедневная подборка фото.",
|
"scheduledAssets": "Отправляет случайные или выбранные фото из альбомов по расписанию. Как ежедневная подборка фото.",
|
||||||
"memoryMode": "\"В этот день\" — отправляет фото, сделанные в этот день в прошлые годы. Ностальгические воспоминания.",
|
"memoryMode": "\"В этот день\" — отправляет фото, сделанные в этот день в прошлые годы. Ностальгические воспоминания.",
|
||||||
"memorySource": "Альбомы: сканирует отслеживаемые альбомы по дате. Встроенные: использует воспоминания Immich (вся библиотека, с фильтрацией по альбомам).",
|
"memorySource": "Альбомы: сканирует отслеживаемые альбомы по дате. Встроенные: использует воспоминания Immich (вся библиотека, с фильтрацией по альбомам).",
|
||||||
|
"quietHours": "Подавляет все уведомления в указанном HH:MM окне (по часовому поясу приложения). Поддерживаются окна через полночь, например 22:00–07:00.",
|
||||||
"favoritesOnly": "Включать только ассеты, отмеченные как избранные.",
|
"favoritesOnly": "Включать только ассеты, отмеченные как избранные.",
|
||||||
"maxAssets": "Максимальное количество ассетов в одном уведомлении.",
|
"maxAssets": "Максимальное количество ассетов в одном уведомлении.",
|
||||||
"periodicStartDate": "Опорная дата для расчёта интервалов. Сводки отправляются каждые N дней от этой даты.",
|
"periodicStartDate": "Опорная дата для расчёта интервалов. Сводки отправляются каждые N дней от этой даты.",
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ export const immichDescriptor: ProviderDescriptor = {
|
|||||||
{ key: 'memory_source', label: 'trackingConfig.memorySource', type: 'grid-select', gridItems: 'memorySourceItems', gridColumns: 2, defaultValue: 'albums' },
|
{ key: 'memory_source', label: 'trackingConfig.memorySource', type: 'grid-select', gridItems: 'memorySourceItems', gridColumns: 2, defaultValue: 'albums' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'quietHours', legend: 'trackingConfig.quietHours', legendHint: 'hints.quietHours',
|
||||||
|
enabledField: 'quiet_hours_enabled', enabledDefault: false,
|
||||||
|
fields: [
|
||||||
|
{ key: 'quiet_hours_start', label: 'trackingConfig.quietHoursStart', type: 'number', defaultValue: '22:00' },
|
||||||
|
{ key: 'quiet_hours_end', label: 'trackingConfig.quietHoursEnd', type: 'number', defaultValue: '07:00' },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
collectionMeta: {
|
collectionMeta: {
|
||||||
|
|||||||
@@ -192,6 +192,9 @@ export interface TrackingConfig {
|
|||||||
memory_favorite_only: boolean;
|
memory_favorite_only: boolean;
|
||||||
memory_asset_type: string;
|
memory_asset_type: string;
|
||||||
memory_min_rating: number;
|
memory_min_rating: number;
|
||||||
|
quiet_hours_enabled: boolean;
|
||||||
|
quiet_hours_start: string | null;
|
||||||
|
quiet_hours_end: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
telegram_webhook_secret: '',
|
telegram_webhook_secret: '',
|
||||||
telegram_cache_ttl_hours: '48',
|
telegram_cache_ttl_hours: '48',
|
||||||
supported_locales: 'en,ru',
|
supported_locales: 'en,ru',
|
||||||
|
timezone: 'UTC',
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@@ -57,6 +58,11 @@
|
|||||||
<input bind:value={settings.external_url} placeholder="https://notify.example.com"
|
<input bind:value={settings.external_url} placeholder="https://notify.example.com"
|
||||||
class="w-full max-w-md px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-background)] font-mono" />
|
class="w-full max-w-md px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-background)] font-mono" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium mb-1">{t('settings.timezone')}<Hint text={t('settings.timezoneHint')} /></label>
|
||||||
|
<input bind:value={settings.timezone} placeholder="UTC"
|
||||||
|
class="w-full max-w-md px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-background)] font-mono" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -181,9 +181,12 @@
|
|||||||
{:else if field.type === 'grid-select' && field.gridItems}
|
{:else if field.type === 'grid-select' && field.gridItems}
|
||||||
<IconGridSelect items={gridItemSources[field.gridItems]()} bind:value={form[field.key]} columns={field.gridColumns ?? 2} compact />
|
<IconGridSelect items={gridItemSources[field.gridItems]()} bind:value={form[field.key]} columns={field.gridColumns ?? 2} compact />
|
||||||
{:else}
|
{:else}
|
||||||
<input type={field.key.includes('date') ? 'date' : field.key.includes('times') ? 'text' : 'number'}
|
<input type={field.key.includes('date') ? 'date'
|
||||||
|
: field.key.startsWith('quiet_hours_') ? 'time'
|
||||||
|
: field.key.includes('times') ? 'text'
|
||||||
|
: 'number'}
|
||||||
bind:value={form[field.key]} min={field.min} max={field.max}
|
bind:value={form[field.key]} min={field.min} max={field.max}
|
||||||
placeholder={field.key.includes('times') ? String(field.defaultValue ?? '') : ''}
|
placeholder={field.key.includes('times') || field.key.startsWith('quiet_hours_') ? String(field.defaultValue ?? '') : ''}
|
||||||
class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ _SETTING_KEYS = {
|
|||||||
"telegram_webhook_secret": "NOTIFY_BRIDGE_TELEGRAM_WEBHOOK_SECRET",
|
"telegram_webhook_secret": "NOTIFY_BRIDGE_TELEGRAM_WEBHOOK_SECRET",
|
||||||
"telegram_cache_ttl_hours": None, # no env fallback, default 48
|
"telegram_cache_ttl_hours": None, # no env fallback, default 48
|
||||||
"supported_locales": None, # comma-separated locale codes
|
"supported_locales": None, # comma-separated locale codes
|
||||||
|
"timezone": "NOTIFY_BRIDGE_TIMEZONE", # IANA tz (e.g. "Europe/Warsaw"); empty = UTC
|
||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULTS = {
|
_DEFAULTS = {
|
||||||
@@ -29,6 +30,7 @@ _DEFAULTS = {
|
|||||||
"telegram_webhook_secret": "",
|
"telegram_webhook_secret": "",
|
||||||
"telegram_cache_ttl_hours": "48",
|
"telegram_cache_ttl_hours": "48",
|
||||||
"supported_locales": "en,ru",
|
"supported_locales": "en,ru",
|
||||||
|
"timezone": "UTC",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ class SettingsUpdate(BaseModel):
|
|||||||
telegram_webhook_secret: str | None = None
|
telegram_webhook_secret: str | None = None
|
||||||
telegram_cache_ttl_hours: str | None = None
|
telegram_cache_ttl_hours: str | None = None
|
||||||
supported_locales: str | None = None
|
supported_locales: str | None = None
|
||||||
|
timezone: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ class TrackingConfigCreate(BaseModel):
|
|||||||
memory_favorite_only: bool = False
|
memory_favorite_only: bool = False
|
||||||
memory_asset_type: str = "all"
|
memory_asset_type: str = "all"
|
||||||
memory_min_rating: int = 0
|
memory_min_rating: int = 0
|
||||||
|
quiet_hours_enabled: bool = False
|
||||||
|
quiet_hours_start: str | None = None
|
||||||
|
quiet_hours_end: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class TrackingConfigUpdate(BaseModel):
|
class TrackingConfigUpdate(BaseModel):
|
||||||
@@ -93,6 +96,9 @@ class TrackingConfigUpdate(BaseModel):
|
|||||||
memory_favorite_only: bool | None = None
|
memory_favorite_only: bool | None = None
|
||||||
memory_asset_type: str | None = None
|
memory_asset_type: str | None = None
|
||||||
memory_min_rating: int | None = None
|
memory_min_rating: int | None = None
|
||||||
|
quiet_hours_enabled: bool | None = None
|
||||||
|
quiet_hours_start: str | None = None
|
||||||
|
quiet_hours_end: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ from ..database.models import (
|
|||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
WebhookPayloadLog,
|
WebhookPayloadLog,
|
||||||
)
|
)
|
||||||
from ..services.dispatch_helpers import event_allowed_by_config, load_link_data
|
from ..services.dispatch_helpers import (
|
||||||
|
event_allowed_by_config,
|
||||||
|
get_app_timezone,
|
||||||
|
load_link_data,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -144,6 +148,8 @@ async def _dispatch_webhook_event(
|
|||||||
if not link_data:
|
if not link_data:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
app_tz = await get_app_timezone(session)
|
||||||
|
|
||||||
# Log event
|
# Log event
|
||||||
extra_details = {k: v for k, v in event.extra.items() if k in detail_keys}
|
extra_details = {k: v for k, v in event.extra.items() if k in detail_keys}
|
||||||
session.add(EventLog(
|
session.add(EventLog(
|
||||||
@@ -164,7 +170,7 @@ async def _dispatch_webhook_event(
|
|||||||
|
|
||||||
# Dispatch to targets
|
# Dispatch to targets
|
||||||
dispatcher = NotificationDispatcher()
|
dispatcher = NotificationDispatcher()
|
||||||
target_configs = _build_target_configs(event, link_data, provider_config)
|
target_configs = _build_target_configs(event, link_data, provider_config, app_tz)
|
||||||
if target_configs:
|
if target_configs:
|
||||||
results = await dispatcher.dispatch(event, target_configs)
|
results = await dispatcher.dispatch(event, target_configs)
|
||||||
for r in results:
|
for r in results:
|
||||||
@@ -513,12 +519,13 @@ def _build_target_configs(
|
|||||||
event: ServiceEvent,
|
event: ServiceEvent,
|
||||||
link_data: list[dict[str, Any]],
|
link_data: list[dict[str, Any]],
|
||||||
provider_config: dict[str, Any],
|
provider_config: dict[str, Any],
|
||||||
|
app_tz: str = "UTC",
|
||||||
) -> list[TargetConfig]:
|
) -> list[TargetConfig]:
|
||||||
"""Build TargetConfig objects for dispatch, applying tracking config filters."""
|
"""Build TargetConfig objects for dispatch, applying tracking config filters."""
|
||||||
target_configs: list[TargetConfig] = []
|
target_configs: list[TargetConfig] = []
|
||||||
for ld in link_data:
|
for ld in link_data:
|
||||||
tc = ld["tracking_config"]
|
tc = ld["tracking_config"]
|
||||||
if tc and not event_allowed_by_config(event, tc):
|
if tc and not event_allowed_by_config(event, tc, app_tz):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tmpl = ld["template_config"]
|
tmpl = ld["template_config"]
|
||||||
|
|||||||
@@ -204,6 +204,13 @@ class TrackingConfig(SQLModel, table=True):
|
|||||||
memory_asset_type: str = Field(default="all")
|
memory_asset_type: str = Field(default="all")
|
||||||
memory_min_rating: int = Field(default=0)
|
memory_min_rating: int = Field(default=0)
|
||||||
|
|
||||||
|
# Quiet hours — HH:MM strings interpreted in the app-level timezone
|
||||||
|
# (AppSetting "timezone"). Gated by quiet_hours_enabled so an empty window
|
||||||
|
# still represents "explicitly disabled" vs "not yet configured".
|
||||||
|
quiet_hours_enabled: bool = Field(default=False)
|
||||||
|
quiet_hours_start: str | None = Field(default=None)
|
||||||
|
quiet_hours_end: str | None = Field(default=None)
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=_utcnow)
|
created_at: datetime = Field(default_factory=_utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, time, timezone
|
from datetime import datetime, time, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -29,12 +30,32 @@ from ..database.models import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def in_quiet_hours(start: str | None, end: str | None) -> bool:
|
def _resolve_zoneinfo(tz_name: str | None) -> ZoneInfo:
|
||||||
"""Check if the current UTC time is within the quiet hours window."""
|
"""Resolve an IANA tz string to a ZoneInfo, falling back to UTC on any error."""
|
||||||
|
if not tz_name:
|
||||||
|
return ZoneInfo("UTC")
|
||||||
|
try:
|
||||||
|
return ZoneInfo(tz_name)
|
||||||
|
except (ZoneInfoNotFoundError, ValueError):
|
||||||
|
_LOGGER.warning("Unknown timezone %r; falling back to UTC", tz_name)
|
||||||
|
return ZoneInfo("UTC")
|
||||||
|
|
||||||
|
|
||||||
|
def in_quiet_hours(
|
||||||
|
start: str | None,
|
||||||
|
end: str | None,
|
||||||
|
tz_name: str | None = "UTC",
|
||||||
|
) -> bool:
|
||||||
|
"""Check if the current time (in the given timezone) is within the quiet window.
|
||||||
|
|
||||||
|
HH:MM strings are interpreted in the supplied timezone. If either bound is
|
||||||
|
missing, quiet hours are disabled.
|
||||||
|
"""
|
||||||
if not start or not end:
|
if not start or not end:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
now = datetime.now(timezone.utc).time()
|
tz = _resolve_zoneinfo(tz_name)
|
||||||
|
now = datetime.now(timezone.utc).astimezone(tz).time()
|
||||||
t_start = time.fromisoformat(start)
|
t_start = time.fromisoformat(start)
|
||||||
t_end = time.fromisoformat(end)
|
t_end = time.fromisoformat(end)
|
||||||
if t_start <= t_end:
|
if t_start <= t_end:
|
||||||
@@ -46,8 +67,25 @@ def in_quiet_hours(start: str | None, end: str | None) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def event_allowed_by_config(event: ServiceEvent, tc: TrackingConfig) -> bool:
|
async def get_app_timezone(session: AsyncSession) -> str:
|
||||||
"""Check if an event type is allowed by the tracking config's flags."""
|
"""Load the app-level timezone from AppSetting (falls back to UTC)."""
|
||||||
|
from ..api.app_settings import get_setting
|
||||||
|
value = await get_setting(session, "timezone")
|
||||||
|
return value or "UTC"
|
||||||
|
|
||||||
|
|
||||||
|
def event_allowed_by_config(
|
||||||
|
event: ServiceEvent,
|
||||||
|
tc: TrackingConfig,
|
||||||
|
tz_name: str | None = "UTC",
|
||||||
|
) -> bool:
|
||||||
|
"""Check if an event is allowed by the tracking config's flags + quiet hours."""
|
||||||
|
# Quiet hours gate every event type when enabled.
|
||||||
|
if tc.quiet_hours_enabled and in_quiet_hours(
|
||||||
|
tc.quiet_hours_start, tc.quiet_hours_end, tz_name
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
event_type = event.event_type.value
|
event_type = event.event_type.value
|
||||||
flag_map = {
|
flag_map = {
|
||||||
# Immich events
|
# Immich events
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ from ..database.models import (
|
|||||||
NotificationTrackerState,
|
NotificationTrackerState,
|
||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
)
|
)
|
||||||
from .dispatch_helpers import event_allowed_by_config, load_link_data
|
from .dispatch_helpers import (
|
||||||
|
event_allowed_by_config,
|
||||||
|
get_app_timezone,
|
||||||
|
load_link_data,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -85,7 +89,10 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Load tracker-target links
|
# Load tracker-target links
|
||||||
link_data = await load_link_data(session, tracker_id, check_quiet_hours=True)
|
link_data = await load_link_data(session, tracker_id)
|
||||||
|
|
||||||
|
# Load app-level timezone for quiet-hours evaluation.
|
||||||
|
app_tz = await get_app_timezone(session)
|
||||||
|
|
||||||
# Snapshot the data we need
|
# Snapshot the data we need
|
||||||
provider_type = provider.type
|
provider_type = provider.type
|
||||||
@@ -236,7 +243,7 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
|
|||||||
for ld in link_data:
|
for ld in link_data:
|
||||||
# Apply per-link event filtering from tracking config
|
# Apply per-link event filtering from tracking config
|
||||||
tc = ld["tracking_config"]
|
tc = ld["tracking_config"]
|
||||||
if tc and not event_allowed_by_config(event, tc):
|
if tc and not event_allowed_by_config(event, tc, app_tz):
|
||||||
_LOGGER.info(" Skipped by tracking config filter")
|
_LOGGER.info(" Skipped by tracking config filter")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user