Updated apps
@@ -0,0 +1,93 @@
|
||||
"""Amazon Order Status integration."""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN, SERVICE_PURGE_ORDER, ATTR_ORDER_ID
|
||||
from .coordinator import AmazonOrdersCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["sensor"]
|
||||
DEFAULT_UPDATE_INTERVAL = 5 # minutes
|
||||
|
||||
PURGE_ORDER_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_ORDER_ID, default=""): cv.string}
|
||||
)
|
||||
|
||||
|
||||
async def _handle_purge_order(hass: HomeAssistant, call: ServiceCall) -> None:
|
||||
"""Remove a specific order from tracking."""
|
||||
order_id = (call.data.get(ATTR_ORDER_ID) or "").strip()
|
||||
if not order_id:
|
||||
_LOGGER.warning(
|
||||
"purge_order called with empty order_id. "
|
||||
"If using a dashboard button, call script.purge_amazon_order instead so the order ID is read when you tap."
|
||||
)
|
||||
return
|
||||
domain_data = hass.data.get(DOMAIN) or {}
|
||||
removed = False
|
||||
for key, value in domain_data.items():
|
||||
if isinstance(value, AmazonOrdersCoordinator):
|
||||
if await value.async_purge_order(order_id):
|
||||
removed = True
|
||||
if not removed:
|
||||
_LOGGER.warning("Order %s not found or already purged", order_id)
|
||||
|
||||
|
||||
def _make_purge_order_handler(hass: HomeAssistant):
|
||||
"""Return an async service handler that closes over hass."""
|
||||
|
||||
async def handler(call: ServiceCall) -> None:
|
||||
await _handle_purge_order(hass, call)
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Amazon Order Status from a config entry."""
|
||||
# Create the coordinator
|
||||
coordinator = AmazonOrdersCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Ensure DOMAIN dict exists
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Store coordinator under a fixed key for options_flow
|
||||
hass.data[DOMAIN]["coordinator"] = coordinator
|
||||
|
||||
# Also store by entry_id for platform setup
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
# Register purge_order service (idempotent if multiple entries)
|
||||
if not hass.services.has_service(DOMAIN, SERVICE_PURGE_ORDER):
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_PURGE_ORDER,
|
||||
_make_purge_order_handler(hass),
|
||||
schema=PURGE_ORDER_SCHEMA,
|
||||
)
|
||||
|
||||
# Forward entry setups (sensors)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
# Unload platforms
|
||||
await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
# Remove coordinator references
|
||||
if DOMAIN in hass.data:
|
||||
if "coordinator" in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN].pop("coordinator")
|
||||
if entry.entry_id in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Config flow for Amazon Order Status integration."""
|
||||
|
||||
from homeassistant import config_entries
|
||||
import voluptuous as vol
|
||||
import imaplib
|
||||
import socket
|
||||
|
||||
from .const import DOMAIN, CONF_IMAP_FOLDER
|
||||
from .options_flow import AmazonOrderStatusOptionsFlow
|
||||
|
||||
|
||||
def _select_folder_quoted(imap, folder: str) -> None:
|
||||
"""Select IMAP mailbox using quoted name (IMAP4rev2-compatible)."""
|
||||
quoted = '"' + folder.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
imap._simple_command("SELECT", quoted.encode("utf-8"))
|
||||
|
||||
|
||||
async def validate_imap_config(hass, host, port, username, password, folder=None):
|
||||
"""Test connection to IMAP server."""
|
||||
def _validate():
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(host, port)
|
||||
imap.login(username, password)
|
||||
folder_to_test = folder.strip() if folder and folder.strip() else "INBOX"
|
||||
_select_folder_quoted(imap, folder_to_test)
|
||||
imap.logout()
|
||||
return None
|
||||
except imaplib.IMAP4.error:
|
||||
return "invalid_auth"
|
||||
except (OSError, socket.gaierror):
|
||||
return "cannot_connect"
|
||||
|
||||
return await hass.async_add_executor_job(_validate)
|
||||
|
||||
|
||||
class AmazonOrdersConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Amazon Orders."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step of the config flow."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# Validate IMAP connection
|
||||
error = await validate_imap_config(
|
||||
self.hass,
|
||||
user_input["imap_server"],
|
||||
user_input.get("imap_port", 993),
|
||||
user_input["username"],
|
||||
user_input["password"],
|
||||
user_input.get(CONF_IMAP_FOLDER),
|
||||
)
|
||||
|
||||
if error is None:
|
||||
# Create config entry with initial options including mark_as_read
|
||||
return self.async_create_entry(
|
||||
title="Amazon Orders",
|
||||
data={
|
||||
"email": user_input["email"],
|
||||
"imap_server": user_input["imap_server"],
|
||||
"username": user_input["username"],
|
||||
"password": user_input["password"],
|
||||
"imap_port": user_input.get("imap_port", 993),
|
||||
},
|
||||
options={
|
||||
"update_interval": user_input.get("poll_interval", 5),
|
||||
"delivered_retention_days": 30,
|
||||
"mark_as_read": user_input.get("mark_as_read", True),
|
||||
CONF_IMAP_FOLDER: user_input.get(CONF_IMAP_FOLDER, ""),
|
||||
},
|
||||
)
|
||||
|
||||
errors["base"] = error
|
||||
|
||||
# Show the form with mark_as_read option
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required("email"): str,
|
||||
vol.Required("imap_server"): str,
|
||||
vol.Required("username"): str,
|
||||
vol.Required("password"): str,
|
||||
vol.Optional("imap_port", default=993): int,
|
||||
vol.Optional("poll_interval", default=5): int,
|
||||
vol.Optional("mark_as_read", default=True): bool,
|
||||
vol.Optional(CONF_IMAP_FOLDER, default=""): str,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Return the options flow handler for this config entry."""
|
||||
return AmazonOrderStatusOptionsFlow(config_entry)
|
||||
@@ -0,0 +1,18 @@
|
||||
DOMAIN = "amazon_order_status"
|
||||
|
||||
CONF_EMAIL = "email"
|
||||
CONF_IMAP_SERVER = "imap_server"
|
||||
CONF_IMAP_PORT = "imap_port"
|
||||
CONF_USERNAME = "username"
|
||||
CONF_PASSWORD = "password"
|
||||
CONF_POLL_INTERVAL = "poll_interval"
|
||||
CONF_MARK_AS_READ = "mark_as_read"
|
||||
CONF_DELIVERED_RETENTION_DAYS = "delivered_retention_days"
|
||||
CONF_IMAP_FOLDER = "imap_folder"
|
||||
ERROR_CANNOT_CONNECT = "cannot_connect"
|
||||
ERROR_INVALID_AUTH = "invalid_auth"
|
||||
|
||||
SERVICE_PURGE_ORDER = "purge_order"
|
||||
ATTR_ORDER_ID = "order_id"
|
||||
|
||||
DEFAULT_POLL_INTERVAL = 1800 # 30 minutes
|
||||
@@ -0,0 +1,488 @@
|
||||
"""Amazon Orders Data Coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import imaplib
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import email.utils
|
||||
from email import message_from_bytes
|
||||
from email.header import decode_header
|
||||
from typing import Dict
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.storage import Store
|
||||
import html
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from .const import CONF_MARK_AS_READ, CONF_IMAP_FOLDER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_internaldate(internaldate_str: str) -> datetime | None:
|
||||
"""Parse IMAP INTERNALDATE string to timezone-aware UTC datetime."""
|
||||
try:
|
||||
# Format: "08-Feb-2025 18:30:00 +0000" or "08-Feb-2025 10:30:00 -0800"
|
||||
internaldate_str = internaldate_str.strip()
|
||||
if len(internaldate_str) < 26:
|
||||
return None
|
||||
dt = datetime.strptime(internaldate_str[:20], "%d-%b-%Y %H:%M:%S")
|
||||
tz_str = internaldate_str[20:].strip()
|
||||
if not tz_str:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
sign = -1 if tz_str[0] == "-" else 1
|
||||
hours = int(tz_str[1:3])
|
||||
mins = int(tz_str[3:5]) if len(tz_str) >= 5 else 0
|
||||
tz = timezone(timedelta(minutes=sign * (hours * 60 + mins)))
|
||||
return dt.replace(tzinfo=tz).astimezone(timezone.utc)
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _to_utc(dt: datetime) -> datetime:
|
||||
"""Normalize a datetime to UTC for comparison."""
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
LAST_CHECK_KEY = "last_check"
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = "amazon_order_status"
|
||||
ORDERS_KEY = "orders"
|
||||
|
||||
ORDER_REGEX = re.compile(
|
||||
r"(?:Order|Purchase)\s*(?:#|number|ID|No\.?)?\s*[:#]?\s*"
|
||||
r"([0-9]{3}-[0-9]{7}-[0-9]{7}|[0-9\-]{10,})",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# IMAP INTERNALDATE format: "08-Feb-2025 18:30:00 +0000"
|
||||
INTERNALDATE_RE = re.compile(r'INTERNALDATE\s+"([^"]+)"', re.IGNORECASE)
|
||||
|
||||
# English month abbreviations for IMAP date (RFC 3501); avoid locale-dependent strftime
|
||||
_IMAP_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
||||
|
||||
|
||||
def _imap_date_str(dt: datetime) -> str:
|
||||
"""Return date in IMAP format dd-Mon-yyyy (English month) for SEARCH SINCE."""
|
||||
return f"{dt.day:02d}-{_IMAP_MONTHS[dt.month - 1]}-{dt.year}"
|
||||
|
||||
STATUS_MAP = {
|
||||
"successfully placed your order": "Ordered",
|
||||
"we've received your order": "Ordered",
|
||||
"preparing your automatic refill order": "Ordered",
|
||||
"automatic refill order": "Ordered",
|
||||
"ordered": "Ordered",
|
||||
"shipped": "Shipped",
|
||||
"out for delivery": "Out for delivery",
|
||||
"delivered": "Delivered",
|
||||
}
|
||||
|
||||
def _select_folder(mail: imaplib.IMAP4, folder: str) -> None:
|
||||
"""Select an IMAP mailbox. Use standard select() when safe; otherwise send SELECT line ourselves.
|
||||
|
||||
For names with space or parentheses, imaplib sends SELECT Test folder (unquoted), which
|
||||
FastMail rejects as 'Invalid modifier list'. Sending bytes via _command can be treated
|
||||
as a literal. So we build and send the exact line: TAG SELECT "folder"\r\n via mail.send(),
|
||||
then wait for the response and set state.
|
||||
"""
|
||||
if any(c in folder for c in " ()"):
|
||||
mail.untagged_responses = {}
|
||||
quoted = '"' + folder.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
tag = mail._new_tag()
|
||||
tag_str = tag.decode("ascii") if isinstance(tag, bytes) else tag
|
||||
line = tag_str + " SELECT " + quoted + "\r\n"
|
||||
mail.send(line.encode("utf-8"))
|
||||
typ, data = mail._command_complete("SELECT", tag)
|
||||
if typ != "OK":
|
||||
msg = data[-1].decode("utf-8", "replace") if data and isinstance(data[-1], bytes) else str(data)
|
||||
raise mail.error(
|
||||
"Mailbox %r not found: %s. Check the folder name in integration options "
|
||||
"(case-sensitive). Use INBOX for the main inbox, or try a path like INBOX.Test folder."
|
||||
% (folder, msg)
|
||||
)
|
||||
mail.state = "SELECTED"
|
||||
else:
|
||||
mail.select(folder)
|
||||
|
||||
|
||||
class AmazonOrdersCoordinator(DataUpdateCoordinator):
|
||||
"""Coordinator to fetch and track Amazon orders via email."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
|
||||
self.hass = hass
|
||||
self.entry = entry
|
||||
self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
self._orders: Dict[str, dict] = {}
|
||||
self.delivered_retention_days = entry.options.get("delivered_retention_days", 7)
|
||||
self._mark_as_read = entry.options.get(CONF_MARK_AS_READ, False)
|
||||
# Get IMAP folder from options, default to "INBOX" if empty or not set
|
||||
folder = entry.options.get(CONF_IMAP_FOLDER, "")
|
||||
self._imap_folder = folder.strip() if folder and folder.strip() else "INBOX"
|
||||
self.last_check: datetime | None = None
|
||||
|
||||
# Determine update interval from options or config entry, default 5 min
|
||||
interval_minutes = entry.options.get(
|
||||
"update_interval", entry.data.get("update_interval", 5)
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Amazon Order Status",
|
||||
update_interval=timedelta(minutes=interval_minutes),
|
||||
)
|
||||
|
||||
""" Timestamp storage/retrieval functions to reduce email check time """
|
||||
async def async_load_last_check(self) -> datetime | None:
|
||||
stored = await self._store.async_load()
|
||||
if stored and LAST_CHECK_KEY in stored:
|
||||
return datetime.fromisoformat(stored[LAST_CHECK_KEY])
|
||||
return None
|
||||
|
||||
async def async_load_stored_orders(self) -> None:
|
||||
"""Load persisted orders from storage."""
|
||||
stored = await self._store.async_load()
|
||||
if stored:
|
||||
self._orders = stored.get(ORDERS_KEY, {})
|
||||
_LOGGER.debug("Loaded %d stored Amazon orders", len(self._orders))
|
||||
else:
|
||||
self._orders = {}
|
||||
_LOGGER.debug("No stored Amazon orders found")
|
||||
|
||||
async def async_save_state(self, last_check: datetime) -> None:
|
||||
await self._store.async_save(
|
||||
{
|
||||
LAST_CHECK_KEY: last_check.isoformat(),
|
||||
ORDERS_KEY: self._orders,
|
||||
}
|
||||
)
|
||||
|
||||
async def _async_update_data(self):
|
||||
_LOGGER.debug("Coordinator update triggered at %s", datetime.now(timezone.utc))
|
||||
|
||||
if not self._orders:
|
||||
await self.async_load_stored_orders()
|
||||
|
||||
last_check = await self.async_load_last_check()
|
||||
now = datetime.now(timezone.utc)
|
||||
self.last_check = now
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self._fetch_and_parse_emails,
|
||||
last_check,
|
||||
now,
|
||||
)
|
||||
|
||||
# Purge old delivered orders
|
||||
self._purge_old_delivered_orders(now)
|
||||
|
||||
await self.async_save_state(now)
|
||||
|
||||
# Include order_id in each item so sensors and services can use it
|
||||
return [{**v, "order_id": k} for k, v in self._orders.items()]
|
||||
|
||||
@callback
|
||||
def async_update_interval(self, minutes: int):
|
||||
"""Dynamically update the coordinator's refresh interval."""
|
||||
self.update_interval = timedelta(minutes=minutes)
|
||||
_LOGGER.debug("Coordinator update interval set to %d minutes", minutes)
|
||||
self.hass.async_create_task(self.async_refresh())
|
||||
|
||||
@callback
|
||||
def async_set_retention_days(self, days: int):
|
||||
"""Update delivered retention days and immediately purge old delivered orders."""
|
||||
self.delivered_retention_days = days
|
||||
_LOGGER.debug("Delivered retention days updated to %d", days)
|
||||
self._purge_old_delivered_orders(datetime.now(timezone.utc))
|
||||
|
||||
@callback
|
||||
def async_set_mark_as_read(self, mark_as_read: bool):
|
||||
"""Enable or disable marking emails as read."""
|
||||
self._mark_as_read = mark_as_read
|
||||
_LOGGER.debug("Mark as read option updated: %s", mark_as_read)
|
||||
|
||||
# Update the config entry safely
|
||||
new_options = dict(self.entry.options)
|
||||
new_options[CONF_MARK_AS_READ] = mark_as_read
|
||||
self.hass.config_entries.async_update_entry(self.entry, options=new_options)
|
||||
|
||||
@callback
|
||||
def async_set_imap_folder(self, folder: str):
|
||||
"""Update the IMAP folder to search."""
|
||||
folder_clean = folder.strip() if folder and folder.strip() else "INBOX"
|
||||
self._imap_folder = folder_clean
|
||||
_LOGGER.debug("IMAP folder updated to: %s", self._imap_folder)
|
||||
|
||||
# Update the config entry safely
|
||||
new_options = dict(self.entry.options)
|
||||
new_options[CONF_IMAP_FOLDER] = folder
|
||||
self.hass.config_entries.async_update_entry(self.entry, options=new_options)
|
||||
|
||||
@callback
|
||||
def _purge_old_delivered_orders(self, now: datetime):
|
||||
"""Remove delivered orders older than retention period."""
|
||||
if not self._orders:
|
||||
return
|
||||
|
||||
retention_cutoff = now - timedelta(days=self.delivered_retention_days)
|
||||
to_remove = [
|
||||
order_id
|
||||
for order_id, order in self._orders.items()
|
||||
if order.get("status") == "Delivered"
|
||||
and datetime.fromisoformat(order.get("updated")) < retention_cutoff
|
||||
]
|
||||
|
||||
for order_id in to_remove:
|
||||
_LOGGER.debug(
|
||||
"Purging delivered order %s (older than %d days)",
|
||||
order_id,
|
||||
self.delivered_retention_days,
|
||||
)
|
||||
self._orders.pop(order_id, None)
|
||||
|
||||
async def async_purge_order(self, order_id: str) -> bool:
|
||||
"""Remove a specific order from tracking and persist state. Returns True if removed."""
|
||||
if order_id not in self._orders:
|
||||
return False
|
||||
self._orders.pop(order_id, None)
|
||||
_LOGGER.debug("Purged order %s by user request", order_id)
|
||||
now = self.last_check or datetime.now(timezone.utc)
|
||||
await self.async_save_state(now)
|
||||
self.async_set_updated_data(
|
||||
[{**v, "order_id": k} for k, v in self._orders.items()]
|
||||
)
|
||||
return True
|
||||
|
||||
def _fetch_and_parse_emails(self, last_check: datetime | None, now: datetime):
|
||||
"""Connect to IMAP and parse Amazon emails."""
|
||||
email_addr = self.entry.data["email"]
|
||||
password = self.entry.data["password"]
|
||||
imap_server = self.entry.data["imap_server"]
|
||||
mark_as_read = self._mark_as_read
|
||||
|
||||
_LOGGER.debug("Connecting to IMAP server %s as %s", imap_server, email_addr)
|
||||
|
||||
mail = imaplib.IMAP4_SSL(imap_server)
|
||||
mail.login(email_addr, password)
|
||||
# Send SELECT with mailbox as a quoted string so IMAP4rev2 servers (e.g. FastMail)
|
||||
# do not misparse the command as having an invalid modifier list (RFC 9051).
|
||||
_select_folder(mail, self._imap_folder)
|
||||
_LOGGER.debug("Selected IMAP folder: %s", self._imap_folder)
|
||||
|
||||
if last_check:
|
||||
since = last_check
|
||||
_LOGGER.debug("Checking emails since last run: %s", since)
|
||||
else:
|
||||
since = now - timedelta(days=14)
|
||||
_LOGGER.debug("First run: checking last 14 days")
|
||||
|
||||
since_utc = _to_utc(since)
|
||||
# IMAP SINCE is interpreted in the server's timezone (e.g. Gmail uses PST), so
|
||||
# using the UTC date can ask for "future" emails and return 0. Use one day
|
||||
# earlier so we always fetch recent emails; we filter by since_utc in Python.
|
||||
since_date_imap = _imap_date_str(since_utc - timedelta(days=1))
|
||||
# No charset for date-only criterion; some servers reject CHARSET with SINCE
|
||||
typ, data = mail.search(None, f'(SINCE "{since_date_imap}")')
|
||||
|
||||
if typ != "OK":
|
||||
_LOGGER.error("IMAP search failed")
|
||||
mail.logout()
|
||||
return
|
||||
|
||||
email_nums = data[0].split()
|
||||
_LOGGER.debug(
|
||||
"Found %d emails since %s (processing all; applying updates when email is newer than stored)",
|
||||
len(email_nums),
|
||||
since_date_imap,
|
||||
)
|
||||
|
||||
for num in email_nums:
|
||||
typ, msg_data = mail.fetch(num, "(BODY.PEEK[] INTERNALDATE)")
|
||||
if typ != "OK" or not msg_data or not msg_data[0]:
|
||||
_LOGGER.warning("Failed to fetch email %s", num)
|
||||
continue
|
||||
|
||||
# Prefer INTERNALDATE (when mailbox received the message) over Date header
|
||||
raw_response = msg_data[0][0]
|
||||
if isinstance(raw_response, bytes):
|
||||
raw_response = raw_response.decode(errors="ignore")
|
||||
internaldate_utc = None
|
||||
internaldate_match = INTERNALDATE_RE.search(raw_response)
|
||||
if internaldate_match:
|
||||
internaldate_utc = _parse_internaldate(internaldate_match.group(1))
|
||||
|
||||
msg = message_from_bytes(msg_data[0][1])
|
||||
msg_date = msg.get("Date")
|
||||
msg_datetime = None
|
||||
if msg_date:
|
||||
msg_datetime = email.utils.parsedate_to_datetime(msg_date)
|
||||
if msg_datetime.tzinfo is None:
|
||||
msg_datetime = msg_datetime.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
msg_datetime = msg_datetime.astimezone(timezone.utc)
|
||||
|
||||
received_utc = internaldate_utc if internaldate_utc is not None else msg_datetime
|
||||
if received_utc is None:
|
||||
_LOGGER.debug("Email %s: no INTERNALDATE or Date header, skipping", num)
|
||||
continue
|
||||
|
||||
# Ignore emails that arrived before our last check (avoids re-adding purged orders)
|
||||
if last_check is not None and received_utc < since_utc:
|
||||
_LOGGER.debug(
|
||||
"Email %s: received %s before last check %s, skipping",
|
||||
num,
|
||||
received_utc,
|
||||
since_utc,
|
||||
)
|
||||
continue
|
||||
|
||||
subject = self._decode_header(msg.get("Subject", ""))
|
||||
subject_lower = subject.lower()
|
||||
|
||||
status = self._status_from_subject(subject_lower)
|
||||
if not status:
|
||||
_LOGGER.debug(
|
||||
"Email %s: subject not recognized as order status, skipping: %s",
|
||||
num,
|
||||
subject[:80],
|
||||
)
|
||||
continue
|
||||
|
||||
body_text = self._extract_text(msg)
|
||||
html_body = self._extract_html(msg)
|
||||
|
||||
# Collect order IDs from both plain text and HTML; many "shipped" emails
|
||||
# put the order number only in the HTML part, so we must check both.
|
||||
order_ids = list(
|
||||
dict.fromkeys(
|
||||
ORDER_REGEX.findall(body_text)
|
||||
+ (ORDER_REGEX.findall(html_body) if html_body else [])
|
||||
)
|
||||
)
|
||||
if not order_ids:
|
||||
_LOGGER.debug(
|
||||
"No order numbers found in email (subject: %s). "
|
||||
"Checked plain text (%d chars) and HTML (%d chars).",
|
||||
subject,
|
||||
len(body_text),
|
||||
len(html_body) if html_body else 0,
|
||||
)
|
||||
continue
|
||||
|
||||
tracking_url = self._extract_tracking_url(html_body) if html_body else None
|
||||
|
||||
updated_ts = (msg_datetime if msg_datetime is not None else received_utc).isoformat()
|
||||
did_update = False
|
||||
for order_id in order_ids:
|
||||
# Only overwrite if we don't have this order or this email is newer than stored
|
||||
existing = self._orders.get(order_id)
|
||||
if existing:
|
||||
try:
|
||||
existing_updated = _to_utc(
|
||||
datetime.fromisoformat(existing["updated"])
|
||||
)
|
||||
if received_utc < existing_updated:
|
||||
_LOGGER.debug(
|
||||
"Order %s: skipping (email %s older than stored)",
|
||||
order_id,
|
||||
received_utc,
|
||||
)
|
||||
continue
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
self._orders[order_id] = {
|
||||
"status": status,
|
||||
"subject": subject,
|
||||
"updated": updated_ts,
|
||||
"tracking_url": tracking_url,
|
||||
}
|
||||
did_update = True
|
||||
_LOGGER.debug(
|
||||
"Order %s → %s (%s) [tracking: %s]",
|
||||
order_id,
|
||||
status,
|
||||
subject,
|
||||
tracking_url,
|
||||
)
|
||||
|
||||
if mark_as_read:
|
||||
_LOGGER.debug("Marking email %s as read (order email processed)", num)
|
||||
mail.store(num, "+FLAGS", "\\Seen")
|
||||
|
||||
mail.logout()
|
||||
|
||||
def _status_from_subject(self, subject: str) -> str | None:
|
||||
"""Determine order status from email subject."""
|
||||
for key, value in STATUS_MAP.items():
|
||||
if key in subject:
|
||||
return value
|
||||
return None
|
||||
|
||||
def _decode_header(self, value: str) -> str:
|
||||
"""Decode email headers safely."""
|
||||
parts = decode_header(value)
|
||||
decoded = ""
|
||||
for text, encoding in parts:
|
||||
if isinstance(text, bytes):
|
||||
decoded += text.decode(encoding or "utf-8", errors="ignore")
|
||||
else:
|
||||
decoded += text
|
||||
return decoded
|
||||
|
||||
def _extract_text(self, msg) -> str:
|
||||
"""Extract flattened text from email."""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() in ("text/plain", "text/html"):
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
return payload.decode(errors="ignore")
|
||||
else:
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
return payload.decode(errors="ignore")
|
||||
return ""
|
||||
|
||||
def _extract_html(self, msg) -> str:
|
||||
"""Extract HTML body from email."""
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/html":
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
return payload.decode(errors="ignore")
|
||||
else:
|
||||
if msg.get_content_type() == "text/html":
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
return payload.decode(errors="ignore")
|
||||
return ""
|
||||
|
||||
def _extract_tracking_url(self, html_body: str) -> str | None:
|
||||
"""Extract Amazon tracking URL or order link from email HTML."""
|
||||
soup = BeautifulSoup(html_body, "html.parser")
|
||||
|
||||
for link in soup.find_all("a", href=True):
|
||||
text = link.get_text(" ", strip=True).lower()
|
||||
href = html.unescape(link["href"])
|
||||
|
||||
# Case 1: Tracking links
|
||||
if "track package" in text or "progress-tracker" in href:
|
||||
match = re.search(
|
||||
r"https://www\.amazon\.com/progress-tracker/[^&\"]+",
|
||||
href,
|
||||
)
|
||||
if match:
|
||||
return match.group(0)
|
||||
return href
|
||||
|
||||
# Case 2: Order management links
|
||||
if "your-orders" in href and ("view" in text or "edit order" in text):
|
||||
return href
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "amazon_order_status",
|
||||
"name": "Amazon Order Status",
|
||||
"codeowners": ["@koconnorgit"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/koconnorgit/ha-amazon-order-status",
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/koconnorgit/ha-amazon-order-status/issues",
|
||||
"requirements": ["beautifulsoup4"],
|
||||
"version": "1.4.5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Options flow for Amazon Order Status integration."""
|
||||
|
||||
from homeassistant import config_entries
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN, CONF_IMAP_FOLDER
|
||||
|
||||
|
||||
class AmazonOrderStatusOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Handle an options flow for Amazon Order Status."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||
self._config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the initial step of the options flow."""
|
||||
if user_input is not None:
|
||||
# Merge new options with existing ones
|
||||
new_options = dict(self._config_entry.options)
|
||||
new_options.update(user_input)
|
||||
|
||||
# Update the config entry
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self._config_entry, options=new_options
|
||||
)
|
||||
|
||||
# Apply updates to the coordinator if it exists
|
||||
coordinator = self.hass.data.get(DOMAIN, {}).get("coordinator")
|
||||
if coordinator:
|
||||
if "delivered_retention_days" in user_input:
|
||||
coordinator.async_set_retention_days(
|
||||
user_input["delivered_retention_days"]
|
||||
)
|
||||
if "update_interval" in user_input:
|
||||
coordinator.async_update_interval(
|
||||
user_input["update_interval"]
|
||||
)
|
||||
if "mark_as_read" in user_input:
|
||||
coordinator.async_set_mark_as_read(
|
||||
user_input["mark_as_read"]
|
||||
)
|
||||
if CONF_IMAP_FOLDER in user_input:
|
||||
coordinator.async_set_imap_folder(
|
||||
user_input[CONF_IMAP_FOLDER]
|
||||
)
|
||||
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
# Current options to use as defaults
|
||||
options = self._config_entry.options
|
||||
|
||||
# Standard vol.Schema
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
"delivered_retention_days",
|
||||
default=options.get("delivered_retention_days", 30),
|
||||
): cv.positive_int,
|
||||
vol.Required(
|
||||
"update_interval",
|
||||
default=options.get("update_interval", 5),
|
||||
): cv.positive_int,
|
||||
vol.Required(
|
||||
"mark_as_read",
|
||||
default=options.get("mark_as_read", True),
|
||||
): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_IMAP_FOLDER,
|
||||
default=options.get(CONF_IMAP_FOLDER, ""),
|
||||
): str,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=schema,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copy this script into Home Assistant so the Purge order button works.
|
||||
#
|
||||
# Settings → Automations & scenes → Scripts → Add script → (paste in YAML mode)
|
||||
#
|
||||
# IMPORTANT: Use "data_template" so the order_id is read when you tap the button.
|
||||
# Using "data" would pass the literal template string and the order would not be found.
|
||||
#
|
||||
purge_amazon_order:
|
||||
alias: Purge Amazon order
|
||||
sequence:
|
||||
- service: amazon_order_status.purge_order
|
||||
data_template:
|
||||
order_id: "{{ states('input_text.amazon_order_purge_id') }}"
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Amazon Orders sensors."""
|
||||
|
||||
import logging
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from datetime import datetime
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from .coordinator import AmazonOrdersCoordinator
|
||||
from homeassistant.util.dt import as_local
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATUSES = [
|
||||
"Ordered",
|
||||
"Shipped",
|
||||
"Out for delivery",
|
||||
"Delivered",
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities,
|
||||
):
|
||||
"""Set up Amazon Order Status sensors."""
|
||||
coordinator: AmazonOrdersCoordinator = hass.data["amazon_order_status"][entry.entry_id]
|
||||
|
||||
sensors = [
|
||||
AmazonOrderStatusSensor(coordinator, status)
|
||||
for status in STATUSES
|
||||
]
|
||||
sensors.append(AmazonOrdersLastUpdatedSensor(coordinator))
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class AmazonOrderStatusSensor(CoordinatorEntity, Entity):
|
||||
"""Sensor representing Amazon orders in a specific status."""
|
||||
|
||||
def __init__(self, coordinator: AmazonOrdersCoordinator, status: str):
|
||||
super().__init__(coordinator)
|
||||
self.status = status
|
||||
self._attr_unique_id = f"amazon_order_status_{status.lower().replace(' ', '_')}"
|
||||
self._attr_name = f"Amazon Orders {status}"
|
||||
self._attr_icon = "mdi:package-variant"
|
||||
|
||||
@property
|
||||
def state(self) -> int:
|
||||
"""Return number of orders in this status."""
|
||||
return len(self._orders_for_status())
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return order details for this status."""
|
||||
orders = self._orders_for_status()
|
||||
return {
|
||||
"order_count": len(orders),
|
||||
"orders": orders,
|
||||
}
|
||||
|
||||
def _orders_for_status(self) -> list[dict]:
|
||||
"""Return orders matching this sensor's status."""
|
||||
if not self.coordinator.data:
|
||||
return []
|
||||
|
||||
return [
|
||||
{
|
||||
"order_id": data.get("order_id"),
|
||||
"subject": data.get("subject"),
|
||||
"updated": data.get("updated"),
|
||||
"tracking_url": data.get("tracking_url"),
|
||||
}
|
||||
for data in self.coordinator.data
|
||||
if data.get("status") == self.status
|
||||
]
|
||||
|
||||
|
||||
class AmazonOrdersLastUpdatedSensor(CoordinatorEntity, Entity):
|
||||
"""Sensor showing when Amazon orders were last updated."""
|
||||
|
||||
def __init__(self, coordinator: AmazonOrdersCoordinator):
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = "amazon_order_status_last_updated"
|
||||
self._attr_name = "Amazon Orders Last Updated"
|
||||
self._attr_icon = "mdi:clock-check-outline"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return ISO timestamp of last successful update."""
|
||||
if not getattr(self.coordinator, "last_check", None):
|
||||
return None
|
||||
return self.coordinator.last_check.isoformat()
|
||||
@@ -0,0 +1,10 @@
|
||||
# Service definitions for Amazon Order Status
|
||||
# https://developers.home-assistant.io/docs/dev_101_services
|
||||
|
||||
purge_order:
|
||||
description: Remove a specific order from tracking. The order will no longer appear in the sensors.
|
||||
fields:
|
||||
order_id:
|
||||
description: The Amazon order number to remove (e.g. 123-4567890-1234567). Copy from sensor attributes.
|
||||
required: true
|
||||
example: "123-4567890-1234567"
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Cannot connect to IMAP server",
|
||||
"invalid_auth": "Invalid username or password"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Amazon Orders",
|
||||
"description": "Configure IMAP access for Amazon order emails. imap_folder is an optional field - leave blank for the root inbox, or provide a folder name to search a specific folder. Note that some email providers require 'INBOX/Folder name', others will accept simply 'Folder name'"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Amazon Order Options",
|
||||
"description": "Configure how Home Assistant tracks and processes Amazon order emails. delivered_retention_days - How many days should HA track delivered orders? update_interval - how often should HA check for delivery updates? imap_folder - Optional email folder to search (leave empty for INBOX)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"purge_order": {
|
||||
"name": "Purge order",
|
||||
"description": "Remove a specific order from tracking. The order will no longer appear in the Amazon Order Status sensors.",
|
||||
"fields": {
|
||||
"order_id": {
|
||||
"name": "Order ID",
|
||||
"description": "The Amazon order number to remove (e.g. 123-4567890-1234567). You can copy this from the sensor's order list."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
"""Mail and Packages Integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_RESOURCES
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import const
|
||||
from .const import (
|
||||
ATTR_IMAGE_NAME,
|
||||
ATTR_IMAGE_PATH,
|
||||
AUTH_TYPE_PASSWORD,
|
||||
CONF_AMAZON_CUSTOM_IMG,
|
||||
CONF_AMAZON_CUSTOM_IMG_FILE,
|
||||
CONF_AMAZON_DAYS,
|
||||
CONF_AMAZON_DOMAIN,
|
||||
CONF_AMAZON_FWDS,
|
||||
CONF_AUTH_TYPE,
|
||||
CONF_FEDEX_CUSTOM_IMG,
|
||||
CONF_FEDEX_CUSTOM_IMG_FILE,
|
||||
CONF_FOLDER,
|
||||
CONF_FORWARDED_EMAILS,
|
||||
CONF_GENERIC_CUSTOM_IMG,
|
||||
CONF_GENERIC_CUSTOM_IMG_FILE,
|
||||
CONF_IMAGE_SECURITY,
|
||||
CONF_IMAP_SECURITY,
|
||||
CONF_IMAP_TIMEOUT,
|
||||
CONF_PATH,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_STORAGE,
|
||||
CONF_UPS_CUSTOM_IMG,
|
||||
CONF_UPS_CUSTOM_IMG_FILE,
|
||||
CONF_VERIFY_SSL,
|
||||
CONF_WALMART_CUSTOM_IMG,
|
||||
CONF_WALMART_CUSTOM_IMG_FILE,
|
||||
CONFIG_VER,
|
||||
DEFAULT_AMAZON_CUSTOM_IMG_FILE,
|
||||
DEFAULT_AMAZON_DAYS,
|
||||
DEFAULT_FEDEX_CUSTOM_IMG_FILE,
|
||||
DEFAULT_GENERIC_CUSTOM_IMG_FILE,
|
||||
DEFAULT_UPS_CUSTOM_IMG_FILE,
|
||||
DEFAULT_WALMART_CUSTOM_IMG_FILE,
|
||||
DOMAIN,
|
||||
ISSUE_URL,
|
||||
PLATFORMS,
|
||||
VERSION,
|
||||
)
|
||||
from .coordinator import (
|
||||
MailAndPackagesConfigEntry,
|
||||
MailAndPackagesData,
|
||||
MailDataUpdateCoordinator,
|
||||
)
|
||||
from .utils.image import default_image_path, hash_file
|
||||
|
||||
__all__ = [
|
||||
"ATTR_IMAGE_NAME",
|
||||
"ATTR_IMAGE_PATH",
|
||||
"AUTH_TYPE_PASSWORD",
|
||||
"CONFIG_VER",
|
||||
"CONF_AMAZON_CUSTOM_IMG",
|
||||
"CONF_AMAZON_CUSTOM_IMG_FILE",
|
||||
"CONF_AMAZON_DAYS",
|
||||
"CONF_AMAZON_DOMAIN",
|
||||
"CONF_AMAZON_FWDS",
|
||||
"CONF_AUTH_TYPE",
|
||||
"CONF_FEDEX_CUSTOM_IMG",
|
||||
"CONF_FEDEX_CUSTOM_IMG_FILE",
|
||||
"CONF_GENERIC_CUSTOM_IMG",
|
||||
"CONF_GENERIC_CUSTOM_IMG_FILE",
|
||||
"CONF_IMAGE_SECURITY",
|
||||
"CONF_IMAP_SECURITY",
|
||||
"CONF_IMAP_TIMEOUT",
|
||||
"CONF_PATH",
|
||||
"CONF_SCAN_INTERVAL",
|
||||
"CONF_STORAGE",
|
||||
"CONF_UPS_CUSTOM_IMG",
|
||||
"CONF_UPS_CUSTOM_IMG_FILE",
|
||||
"CONF_VERIFY_SSL",
|
||||
"CONF_WALMART_CUSTOM_IMG",
|
||||
"CONF_WALMART_CUSTOM_IMG_FILE",
|
||||
"DEFAULT_AMAZON_CUSTOM_IMG_FILE",
|
||||
"DEFAULT_AMAZON_DAYS",
|
||||
"DEFAULT_FEDEX_CUSTOM_IMG_FILE",
|
||||
"DEFAULT_GENERIC_CUSTOM_IMG_FILE",
|
||||
"DEFAULT_UPS_CUSTOM_IMG_FILE",
|
||||
"DEFAULT_WALMART_CUSTOM_IMG_FILE",
|
||||
"DOMAIN",
|
||||
"ISSUE_URL",
|
||||
"PLATFORMS",
|
||||
"VERSION",
|
||||
"MailAndPackagesConfigEntry",
|
||||
"MailAndPackagesData",
|
||||
"MailDataUpdateCoordinator",
|
||||
"const",
|
||||
"default_image_path",
|
||||
"hash_file",
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config_entry: MailAndPackagesConfigEntry): # pylint: disable=unused-argument
|
||||
"""Disallow configuration via YAML."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MailAndPackagesConfigEntry,
|
||||
) -> bool:
|
||||
"""Load the saved entities."""
|
||||
_LOGGER.info(
|
||||
"Version %s is starting, if you have any issues please report them here: %s",
|
||||
VERSION,
|
||||
ISSUE_URL,
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
updated_config = config_entry.data.copy()
|
||||
|
||||
# Sort the resources
|
||||
updated_config[CONF_RESOURCES] = sorted(updated_config[CONF_RESOURCES])
|
||||
|
||||
if updated_config != config_entry.data:
|
||||
hass.config_entries.async_update_entry(config_entry, data=updated_config)
|
||||
|
||||
# Variables for data coordinator
|
||||
config = config_entry.data
|
||||
|
||||
# Setup the data coordinator
|
||||
coordinator = MailDataUpdateCoordinator(hass, config, config_entry)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_refresh()
|
||||
|
||||
# Raise ConfigEntryNotReady if coordinator didn't update
|
||||
if not coordinator.last_update_success:
|
||||
if isinstance(coordinator.last_exception, ConfigEntryAuthFailed):
|
||||
raise coordinator.last_exception
|
||||
exc = coordinator.last_exception
|
||||
detail = (str(exc) or type(exc).__name__) if exc else "unknown error"
|
||||
_LOGGER.error("Error updating sensor data: %s", detail)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
config_entry.runtime_data = MailAndPackagesData(coordinator=coordinator, cameras=[])
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_config_entry_device( # pylint: disable-next=unused-argument
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
device_entry: dr.DeviceEntry,
|
||||
) -> bool:
|
||||
"""Remove config entry from a device if its no longer present."""
|
||||
return not any(
|
||||
identifier
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
and config_entry.runtime_data.get_device(identifier[1])
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MailAndPackagesConfigEntry,
|
||||
) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
_LOGGER.debug("Attempting to unload sensors from the %s integration", DOMAIN)
|
||||
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
for platform in PLATFORMS
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
_LOGGER.debug("Successfully removed sensors from the %s integration", DOMAIN)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(hass, config_entry):
|
||||
"""Migrate an old config entry."""
|
||||
version = config_entry.version
|
||||
new_version = CONFIG_VER
|
||||
|
||||
_LOGGER.debug("Migrating from version %s", version)
|
||||
updated_config = {**config_entry.data}
|
||||
|
||||
_migrate_legacy_versions(updated_config, version, config_entry)
|
||||
_apply_default_config(updated_config)
|
||||
|
||||
if updated_config != config_entry.data:
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=updated_config,
|
||||
version=new_version,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Migration complete to version %s", new_version)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _migrate_legacy_versions(updated_config, version, config_entry):
|
||||
"""Handle migration of legacy versions."""
|
||||
_migrate_versions_1_to_3(updated_config, version, config_entry)
|
||||
_migrate_versions_4_to_16(updated_config, version)
|
||||
_migrate_version_17(updated_config, version)
|
||||
_migrate_version_18(updated_config, version)
|
||||
|
||||
|
||||
def _migrate_versions_1_to_3(updated_config, version, config_entry):
|
||||
"""Handle migration for versions 1 to 3."""
|
||||
if version == 1:
|
||||
if CONF_AMAZON_FWDS in updated_config:
|
||||
if not isinstance(updated_config[CONF_AMAZON_FWDS], list):
|
||||
updated_config[CONF_AMAZON_FWDS] = [
|
||||
x.strip() for x in updated_config[CONF_AMAZON_FWDS].split(",")
|
||||
]
|
||||
else:
|
||||
updated_config[CONF_AMAZON_FWDS] = []
|
||||
else:
|
||||
_LOGGER.warning("Missing configuration data: %s", CONF_AMAZON_FWDS)
|
||||
|
||||
# Force path change
|
||||
updated_config[CONF_PATH] = "custom_components/mail_and_packages/images/"
|
||||
|
||||
# Always on image security
|
||||
if not config_entry.data.get(CONF_IMAGE_SECURITY, False):
|
||||
updated_config[CONF_IMAGE_SECURITY] = True
|
||||
|
||||
# Add default Amazon Days configuration
|
||||
updated_config[CONF_AMAZON_DAYS] = DEFAULT_AMAZON_DAYS
|
||||
|
||||
# 2 -> 4
|
||||
if version <= 2:
|
||||
# Force path change
|
||||
updated_config[CONF_PATH] = "custom_components/mail_and_packages/images/"
|
||||
|
||||
# Always on image security
|
||||
if not config_entry.data.get(CONF_IMAGE_SECURITY, False):
|
||||
updated_config[CONF_IMAGE_SECURITY] = True
|
||||
|
||||
# Add default Amazon Days configuration
|
||||
updated_config[CONF_AMAZON_DAYS] = DEFAULT_AMAZON_DAYS
|
||||
|
||||
if version <= 3:
|
||||
# Add default Amazon Days configuration
|
||||
updated_config[CONF_AMAZON_DAYS] = DEFAULT_AMAZON_DAYS
|
||||
|
||||
|
||||
def _migrate_versions_4_to_16(updated_config, version):
|
||||
"""Handle migration for versions 4 to 16."""
|
||||
_migrate_versions_4_to_7(updated_config, version)
|
||||
_migrate_versions_15_to_16(updated_config, version)
|
||||
|
||||
|
||||
def _migrate_versions_4_to_7(updated_config, version):
|
||||
"""Handle migration for versions 4 to 7."""
|
||||
if version <= 4:
|
||||
if CONF_AMAZON_FWDS in updated_config and updated_config[CONF_AMAZON_FWDS] == [
|
||||
'""',
|
||||
]:
|
||||
updated_config[CONF_AMAZON_FWDS] = []
|
||||
|
||||
if version <= 5:
|
||||
if CONF_VERIFY_SSL not in updated_config:
|
||||
updated_config[CONF_VERIFY_SSL] = True
|
||||
|
||||
if version <= 6:
|
||||
if CONF_IMAP_SECURITY not in updated_config:
|
||||
updated_config[CONF_IMAP_SECURITY] = "SSL"
|
||||
|
||||
if version <= 7:
|
||||
if CONF_AMAZON_DOMAIN not in updated_config:
|
||||
updated_config[CONF_AMAZON_DOMAIN] = "amazon.com"
|
||||
|
||||
|
||||
def _migrate_versions_15_to_16(updated_config, version):
|
||||
"""Handle migration for versions 15 to 16."""
|
||||
if version <= 15:
|
||||
if updated_config.get(CONF_IMAP_SECURITY) == "startTLS":
|
||||
updated_config[CONF_IMAP_SECURITY] = "SSL"
|
||||
if CONF_AUTH_TYPE not in updated_config:
|
||||
updated_config[CONF_AUTH_TYPE] = AUTH_TYPE_PASSWORD
|
||||
|
||||
if version <= 16:
|
||||
if "auth" in updated_config:
|
||||
auth_data = updated_config.pop("auth")
|
||||
updated_config.update(auth_data)
|
||||
|
||||
|
||||
def _migrate_version_17(updated_config, version):
|
||||
"""Handle migration for version 17."""
|
||||
if version <= 17:
|
||||
fwds = updated_config.get(CONF_FORWARDED_EMAILS)
|
||||
if isinstance(fwds, str):
|
||||
if fwds and fwds != "(none)":
|
||||
updated_config[CONF_FORWARDED_EMAILS] = [
|
||||
e.strip() for e in fwds.split(",") if e.strip()
|
||||
]
|
||||
else:
|
||||
updated_config.pop(CONF_FORWARDED_EMAILS, None)
|
||||
|
||||
|
||||
def _migrate_version_18(updated_config, version):
|
||||
"""Handle migration for version 18."""
|
||||
if version <= 18:
|
||||
if CONF_FOLDER in updated_config:
|
||||
folder = updated_config[CONF_FOLDER]
|
||||
if isinstance(folder, str):
|
||||
updated_config[CONF_FOLDER] = folder.strip('"')
|
||||
elif isinstance(folder, (list, tuple, set)):
|
||||
updated_config[CONF_FOLDER] = [
|
||||
f.strip('"') for f in folder if isinstance(f, str)
|
||||
]
|
||||
|
||||
|
||||
def _apply_default_config(updated_config):
|
||||
"""Ensure default configurations are present."""
|
||||
# Require configs on all migration paths
|
||||
if CONF_PATH not in updated_config:
|
||||
updated_config[CONF_PATH] = "custom_components/mail_and_packages/images/"
|
||||
|
||||
if CONF_RESOURCES not in updated_config:
|
||||
updated_config[CONF_RESOURCES] = []
|
||||
|
||||
# Add default for image storage config
|
||||
if CONF_STORAGE not in updated_config:
|
||||
updated_config[CONF_STORAGE] = "custom_components/mail_and_packages/images/"
|
||||
|
||||
_apply_courier_image_defaults(updated_config)
|
||||
_apply_walmart_generic_fedex_defaults(updated_config)
|
||||
|
||||
|
||||
def _apply_courier_image_defaults(updated_config):
|
||||
"""Apply default Amazon and UPS custom image configurations."""
|
||||
if CONF_AMAZON_CUSTOM_IMG not in updated_config:
|
||||
updated_config[CONF_AMAZON_CUSTOM_IMG] = False
|
||||
if CONF_AMAZON_CUSTOM_IMG_FILE not in updated_config:
|
||||
updated_config[CONF_AMAZON_CUSTOM_IMG_FILE] = DEFAULT_AMAZON_CUSTOM_IMG_FILE
|
||||
if CONF_UPS_CUSTOM_IMG not in updated_config:
|
||||
updated_config[CONF_UPS_CUSTOM_IMG] = False
|
||||
if CONF_UPS_CUSTOM_IMG_FILE not in updated_config:
|
||||
updated_config[CONF_UPS_CUSTOM_IMG_FILE] = DEFAULT_UPS_CUSTOM_IMG_FILE
|
||||
|
||||
|
||||
def _apply_walmart_generic_fedex_defaults(updated_config):
|
||||
"""Apply default Walmart, Generic and FedEx custom image configurations."""
|
||||
if CONF_WALMART_CUSTOM_IMG not in updated_config:
|
||||
updated_config[CONF_WALMART_CUSTOM_IMG] = False
|
||||
if CONF_WALMART_CUSTOM_IMG_FILE not in updated_config:
|
||||
updated_config[CONF_WALMART_CUSTOM_IMG_FILE] = DEFAULT_WALMART_CUSTOM_IMG_FILE
|
||||
if CONF_GENERIC_CUSTOM_IMG not in updated_config:
|
||||
updated_config[CONF_GENERIC_CUSTOM_IMG] = False
|
||||
if CONF_GENERIC_CUSTOM_IMG_FILE not in updated_config:
|
||||
updated_config[CONF_GENERIC_CUSTOM_IMG_FILE] = DEFAULT_GENERIC_CUSTOM_IMG_FILE
|
||||
|
||||
if CONF_FEDEX_CUSTOM_IMG not in updated_config:
|
||||
updated_config[CONF_FEDEX_CUSTOM_IMG] = False
|
||||
if CONF_FEDEX_CUSTOM_IMG_FILE not in updated_config:
|
||||
updated_config[CONF_FEDEX_CUSTOM_IMG_FILE] = DEFAULT_FEDEX_CUSTOM_IMG_FILE
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Application credentials for Mail and Packages."""
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
AuthImplementation,
|
||||
AuthorizationServer,
|
||||
ClientCredential,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
OAUTH_SERVERS = {
|
||||
"oauth2_microsoft": AuthorizationServer(
|
||||
authorize_url="https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
token_url="https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||
),
|
||||
"oauth2_google": AuthorizationServer(
|
||||
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
|
||||
token_url="https://oauth2.googleapis.com/token",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_auth_implementation(
|
||||
hass: HomeAssistant,
|
||||
auth_domain: str,
|
||||
credential: ClientCredential,
|
||||
) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
|
||||
"""Return auth implementation based on provider selected in config flow."""
|
||||
# Config flow stores the selected provider before triggering OAuth
|
||||
provider = hass.data[DOMAIN]["oauth_provider"]
|
||||
server = OAUTH_SERVERS[provider]
|
||||
|
||||
return AuthImplementation(
|
||||
hass,
|
||||
auth_domain,
|
||||
credential,
|
||||
server,
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Binary sensors for Mail and Packages."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_RESOURCES
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from . import MailAndPackagesConfigEntry
|
||||
from .const import BINARY_SENSORS, DOMAIN, VERSION
|
||||
from .entity import MailandPackagesBinarySensorEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry: MailAndPackagesConfigEntry, async_add_devices):
|
||||
"""Initialize binary_sensor platform."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
resources = entry.data.get(CONF_RESOURCES, [])
|
||||
|
||||
binary_sensors = [
|
||||
PackagesBinarySensor(value, coordinator, entry)
|
||||
for value in BINARY_SENSORS.values()
|
||||
if not value.selectable or value.key in resources
|
||||
]
|
||||
async_add_devices(binary_sensors, False)
|
||||
|
||||
|
||||
class PackagesBinarySensor(CoordinatorEntity, BinarySensorEntity):
|
||||
"""Implementation of an Mail and Packages binary sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sensor_description: MailandPackagesBinarySensorEntityDescription,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
config: MailAndPackagesConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.coordinator = coordinator
|
||||
self._config = config
|
||||
self.entity_description = sensor_description
|
||||
self._name = sensor_description.name
|
||||
self._type = sensor_description.key
|
||||
self._unique_id = config.entry_id
|
||||
self._host = config.data[CONF_HOST]
|
||||
|
||||
self._attr_name = f"{self._name}"
|
||||
self._attr_unique_id = (
|
||||
f"binary_sensor_{self._host}_{self._type}_{self._unique_id}"
|
||||
)
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
"""Return device information about the mailbox."""
|
||||
return {
|
||||
"connections": {(DOMAIN, self._unique_id)},
|
||||
"name": self._host,
|
||||
"manufacturer": "IMAP E-Mail",
|
||||
"sw_version": VERSION,
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No need to poll. Coordinator notifies entity of updates."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.data is not None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the image is updated."""
|
||||
if self._type in self.coordinator.data:
|
||||
_LOGGER.debug(
|
||||
"binary_sensor: %s value: %s",
|
||||
self._type,
|
||||
self.coordinator.data[self._type],
|
||||
)
|
||||
return bool(self.coordinator.data[self._type])
|
||||
return False
|
||||
@@ -0,0 +1,666 @@
|
||||
"""Camera that loads a picture from a local file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST
|
||||
from homeassistant.core import ServiceCall
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import MailAndPackagesConfigEntry, const
|
||||
from .const import (
|
||||
ATTR_IMAGE_PATH,
|
||||
ATTR_USPS_IMAGE,
|
||||
CAMERA_DATA,
|
||||
CONF_CUSTOM_IMG,
|
||||
CONF_CUSTOM_IMG_FILE,
|
||||
CONF_DURATION,
|
||||
CONF_POST_DE_CUSTOM_IMG,
|
||||
CONF_POST_DE_CUSTOM_IMG_FILE,
|
||||
DOMAIN,
|
||||
SENSOR_NAME,
|
||||
VERSION,
|
||||
)
|
||||
from .utils.image import cleanup_images, generate_delivery_gif, resize_images
|
||||
|
||||
SERVICE_UPDATE_IMAGE = "update_image"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass,
|
||||
config: MailAndPackagesConfigEntry,
|
||||
async_add_entities,
|
||||
):
|
||||
"""Set up the Camera that works with local files."""
|
||||
coordinator = config.runtime_data.coordinator
|
||||
camera = []
|
||||
|
||||
for variable in CAMERA_DATA:
|
||||
temp_cam = MailCam(hass, variable, config, coordinator)
|
||||
camera.append(temp_cam)
|
||||
config.runtime_data.cameras.append(temp_cam)
|
||||
|
||||
async def _update_image(service: ServiceCall) -> None:
|
||||
"""Refresh camera image."""
|
||||
_LOGGER.debug("Updating image: %s", service)
|
||||
cameras = config.runtime_data.cameras
|
||||
entity_id = None
|
||||
|
||||
if ATTR_ENTITY_ID in service.data:
|
||||
entity_id = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
# Update all cameras if no entity_id
|
||||
if entity_id is None:
|
||||
for cam in cameras:
|
||||
await cam.update_file_path()
|
||||
|
||||
else:
|
||||
for cam in cameras:
|
||||
if cam.entity_id in entity_id:
|
||||
await cam.update_file_path()
|
||||
return True
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_IMAGE,
|
||||
_update_image,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_ENTITY_ID): vol.Coerce(str),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
async_add_entities(camera)
|
||||
|
||||
|
||||
class MailCam(CoordinatorEntity, Camera):
|
||||
"""Representation of a local file camera."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
name: str,
|
||||
config: ConfigEntry,
|
||||
coordinator,
|
||||
) -> None:
|
||||
"""Initialize Local File Camera component."""
|
||||
CoordinatorEntity.__init__(self, coordinator)
|
||||
Camera.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self._name = CAMERA_DATA[name][SENSOR_NAME]
|
||||
self._type = name
|
||||
self._host = config.data.get(CONF_HOST)
|
||||
self._unique_id = config.entry_id
|
||||
|
||||
# Derive config keys and default image from camera type name
|
||||
# Remove "_camera" suffix to get base name (e.g., "usps_camera" -> "usps")
|
||||
base_name = self._type.removesuffix("_camera")
|
||||
|
||||
# USPS and Post DE use mail_none.gif, others use no_deliveries_*.jpg
|
||||
if base_name in ("usps", "post_de"):
|
||||
if base_name == "usps":
|
||||
custom_img_key = CONF_CUSTOM_IMG
|
||||
custom_img_file_key = CONF_CUSTOM_IMG_FILE
|
||||
else:
|
||||
custom_img_key = CONF_POST_DE_CUSTOM_IMG
|
||||
custom_img_file_key = CONF_POST_DE_CUSTOM_IMG_FILE
|
||||
default_image = "mail_none.gif"
|
||||
else:
|
||||
# Derive config key names dynamically (e.g., "amazon" -> CONF_AMAZON_CUSTOM_IMG)
|
||||
custom_img_key = getattr(
|
||||
const,
|
||||
f"CONF_{base_name.upper()}_CUSTOM_IMG",
|
||||
None,
|
||||
)
|
||||
custom_img_file_key = getattr(
|
||||
const,
|
||||
f"CONF_{base_name.upper()}_CUSTOM_IMG_FILE",
|
||||
None,
|
||||
)
|
||||
default_image = f"no_deliveries_{base_name}.jpg"
|
||||
|
||||
# Set custom image paths
|
||||
self._no_mail = None
|
||||
if custom_img_key and config.data.get(custom_img_key):
|
||||
self._no_mail = config.data.get(custom_img_file_key)
|
||||
_LOGGER.debug(
|
||||
"%s camera - custom image enabled: %s",
|
||||
self._type,
|
||||
self._no_mail,
|
||||
)
|
||||
|
||||
# Set initial file path based on camera type and custom settings
|
||||
if custom_img_key and config.data.get(custom_img_key):
|
||||
self._file_path = config.data.get(custom_img_file_key)
|
||||
_LOGGER.debug(
|
||||
"%s camera - initial file path set to: %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
else:
|
||||
self._file_path = f"{Path(__file__).parent}/{default_image}"
|
||||
|
||||
# Canonical bundled placeholder for this camera. Always available
|
||||
# regardless of later _file_path changes, so a missing delivery image
|
||||
# can fall back to it instead of returning None (which makes the HA
|
||||
# camera proxy serve HTTP 500).
|
||||
self._default_image_path = f"{Path(__file__).parent}/{default_image}"
|
||||
|
||||
self._cached_image_path: str | None = None
|
||||
self._cached_image_bytes: bytes | None = None
|
||||
self._last_delivery_images: list[str] | None = None
|
||||
self._is_generic: bool = True
|
||||
|
||||
async def async_camera_image(
|
||||
self,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> bytes | None:
|
||||
"""Return image response."""
|
||||
if (
|
||||
self._file_path == self._cached_image_path
|
||||
and self._cached_image_bytes is not None
|
||||
):
|
||||
return self._cached_image_bytes
|
||||
|
||||
_LOGGER.debug(
|
||||
"Camera %s reading image from: %s",
|
||||
self._name,
|
||||
self._file_path,
|
||||
)
|
||||
|
||||
def _read_file(path: str) -> bytes:
|
||||
with Path(path).open("rb") as f:
|
||||
return f.read()
|
||||
|
||||
try:
|
||||
image_bytes = await self.hass.async_add_executor_job(
|
||||
_read_file, self._file_path
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.debug(
|
||||
"Could not read camera %s image from file: %s; "
|
||||
"falling back to bundled placeholder %s",
|
||||
self._name,
|
||||
self._file_path,
|
||||
self._default_image_path,
|
||||
)
|
||||
# Fall back to the bundled placeholder so the camera proxy never
|
||||
# serves HTTP 500 when a delivery image is missing on disk. Skip the
|
||||
# fallback when the primary path already IS the placeholder (true for
|
||||
# non-custom cameras, where _file_path and _default_image_path are
|
||||
# both built from the same default image): re-reading the same
|
||||
# missing file would only fail again, so return None instead.
|
||||
if self._file_path != self._default_image_path:
|
||||
try:
|
||||
image_bytes = await self.hass.async_add_executor_job(
|
||||
_read_file, self._default_image_path
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning(
|
||||
"Camera %s placeholder image also missing: %s",
|
||||
self._name,
|
||||
self._default_image_path,
|
||||
)
|
||||
return None
|
||||
else:
|
||||
# Cache against the placeholder path so repeated reads are
|
||||
# cheap; update_file_path() invalidates the cache when the
|
||||
# primary path changes.
|
||||
self._cached_image_path = self._default_image_path
|
||||
self._cached_image_bytes = image_bytes
|
||||
return image_bytes
|
||||
return None
|
||||
else:
|
||||
self._cached_image_path = self._file_path
|
||||
self._cached_image_bytes = image_bytes
|
||||
return image_bytes
|
||||
|
||||
def check_file_path_access(self, file_path: str) -> None:
|
||||
"""Check that filepath given is readable."""
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.debug(
|
||||
"Could not read camera %s image from file: %s",
|
||||
self._name,
|
||||
file_path,
|
||||
)
|
||||
|
||||
async def update_file_path(self) -> None:
|
||||
"""Update the file_path."""
|
||||
_LOGGER.debug("Camera Update: %s", self._type)
|
||||
_LOGGER.debug("Custom No Mail: %s", self._no_mail)
|
||||
|
||||
if not self.coordinator.last_update_success:
|
||||
_LOGGER.debug("Update to update camera image. Unavailable.")
|
||||
return
|
||||
|
||||
if self.coordinator.data is None:
|
||||
_LOGGER.debug("Unable to update camera image, no data.")
|
||||
return
|
||||
|
||||
if self._type == "usps_camera":
|
||||
self._update_usps_camera()
|
||||
elif self._type == "generic_camera":
|
||||
await self._update_generic_camera()
|
||||
else:
|
||||
await self._update_standard_camera()
|
||||
|
||||
# Invalidate the cache so the next frontend request re-reads the image from disk.
|
||||
# This is necessary because for some shippers (like USPS), the image file path
|
||||
# stays the same but the underlying image content on disk changes.
|
||||
self._cached_image_path = None
|
||||
self._cached_image_bytes = None
|
||||
|
||||
self.check_file_path_access(self._file_path)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_usps_camera(self) -> None:
|
||||
"""Update file path for USPS camera."""
|
||||
self._file_path = f"{Path(__file__).parent}/mail_none.gif"
|
||||
self._is_generic = True
|
||||
required_keys = {ATTR_USPS_IMAGE, ATTR_IMAGE_PATH}
|
||||
if required_keys.issubset(self.coordinator.data):
|
||||
image = self.coordinator.data[ATTR_USPS_IMAGE]
|
||||
path = self.coordinator.data[ATTR_IMAGE_PATH]
|
||||
self._file_path = f"{self.hass.config.path()}/{path}{image}"
|
||||
self._is_generic = not self.coordinator.data.get("usps_update", False)
|
||||
_LOGGER.debug(
|
||||
"usps_camera camera - file path set to: %s",
|
||||
self._file_path,
|
||||
)
|
||||
elif self._no_mail:
|
||||
self._file_path = self._no_mail
|
||||
|
||||
async def _update_generic_camera(self) -> None:
|
||||
"""Update file path for Generic camera."""
|
||||
if self._no_mail:
|
||||
self._file_path = self._no_mail
|
||||
self._is_generic = True
|
||||
_LOGGER.debug("Generic camera - using custom no mail: %s", self._file_path)
|
||||
return
|
||||
|
||||
delivery_images = self._collect_generic_delivery_images()
|
||||
|
||||
if (
|
||||
self._last_delivery_images is not None
|
||||
and delivery_images == self._last_delivery_images
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Generic camera - delivery images unchanged, skipping GIF regeneration"
|
||||
)
|
||||
return
|
||||
|
||||
self._last_delivery_images = delivery_images
|
||||
|
||||
if not delivery_images:
|
||||
self._file_path = f"{Path(__file__).parent}/no_deliveries_generic.jpg"
|
||||
self._is_generic = True
|
||||
_LOGGER.debug(
|
||||
"Generic camera - no deliveries found, using default: %s",
|
||||
self._file_path,
|
||||
)
|
||||
return
|
||||
|
||||
image_path = self.coordinator.data.get(ATTR_IMAGE_PATH, "")
|
||||
full_storage_path = Path(f"{self.hass.config.path()}/{image_path}")
|
||||
gif_path = str(full_storage_path / "generic_deliveries.gif")
|
||||
|
||||
resized_images = await self.hass.async_add_executor_job(
|
||||
resize_images, delivery_images, 800, 600
|
||||
)
|
||||
|
||||
duration = self.config.data.get(CONF_DURATION, 5) * 1000
|
||||
gif_created = await self.hass.async_add_executor_job(
|
||||
generate_delivery_gif,
|
||||
resized_images,
|
||||
gif_path,
|
||||
duration,
|
||||
)
|
||||
|
||||
if gif_created:
|
||||
self._file_path = gif_path
|
||||
self._is_generic = False
|
||||
_LOGGER.debug(
|
||||
"Generic camera - created animated GIF with %d delivery images at %s",
|
||||
len(delivery_images),
|
||||
gif_path,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Failed to create animated GIF, using first delivery image",
|
||||
)
|
||||
self._file_path = delivery_images[0]
|
||||
self._is_generic = False
|
||||
|
||||
for img in resized_images:
|
||||
if await anyio.Path(img).exists():
|
||||
await self.hass.async_add_executor_job(
|
||||
cleanup_images, str(Path(img).parent) + "/", Path(img).name
|
||||
)
|
||||
|
||||
def _collect_generic_delivery_images(self) -> list[str]:
|
||||
"""Collect delivery images for the generic camera."""
|
||||
delivery_images = []
|
||||
enabled_resources = self.config.data.get("resources", [])
|
||||
|
||||
for camera_type in CAMERA_DATA:
|
||||
# Skip generic, USPS, and Post DE cameras
|
||||
if camera_type in ("generic_camera", "usps_camera", "post_de_camera"):
|
||||
continue
|
||||
|
||||
base_name = camera_type.removesuffix("_camera")
|
||||
delivered_key = f"{base_name}_delivered"
|
||||
|
||||
# Check if this shipper's delivery sensor is enabled
|
||||
if delivered_key not in enabled_resources:
|
||||
_LOGGER.debug(
|
||||
"Generic camera - skipping %s (sensor %s not enabled)",
|
||||
base_name,
|
||||
delivered_key,
|
||||
)
|
||||
continue
|
||||
|
||||
# Set image attributes
|
||||
image_attr_name = f"ATTR_{base_name.upper()}_IMAGE"
|
||||
image_attr = getattr(const, image_attr_name, None)
|
||||
path_suffix = f"{base_name}/"
|
||||
no_mail_check = "no_deliveries"
|
||||
|
||||
required_keys = {image_attr, ATTR_IMAGE_PATH}
|
||||
if not required_keys.issubset(self.coordinator.data):
|
||||
continue
|
||||
|
||||
image = self.coordinator.data[image_attr]
|
||||
path = f"{self.coordinator.data[ATTR_IMAGE_PATH]}{path_suffix}"
|
||||
delivery_file_path = f"{self.hass.config.path()}/{path}{image}"
|
||||
|
||||
is_no_mail = image.startswith(
|
||||
no_mail_check,
|
||||
) or self._is_custom_no_mail_image(base_name, delivery_file_path)
|
||||
|
||||
delivery_count_key = f"{base_name}_delivered"
|
||||
has_current_deliveries = (
|
||||
delivery_count_key in self.coordinator.data
|
||||
and self.coordinator.data[delivery_count_key] > 0
|
||||
)
|
||||
|
||||
if (
|
||||
not is_no_mail
|
||||
and Path(delivery_file_path).exists()
|
||||
and has_current_deliveries
|
||||
):
|
||||
delivery_images.append(delivery_file_path)
|
||||
elif is_no_mail:
|
||||
_LOGGER.debug(
|
||||
"Generic camera - filtered out %s no-mail image: %s",
|
||||
base_name,
|
||||
image,
|
||||
)
|
||||
elif not has_current_deliveries:
|
||||
_LOGGER.debug(
|
||||
"Generic camera - filtered out %s (no current deliveries, count=%s): %s",
|
||||
base_name,
|
||||
self.coordinator.data.get(delivery_count_key, 0),
|
||||
image,
|
||||
)
|
||||
|
||||
return delivery_images
|
||||
|
||||
async def _update_standard_camera(self) -> None:
|
||||
"""Update file path for standard cameras (Amazon, UPS, etc)."""
|
||||
base_name = self._type.removesuffix("_camera")
|
||||
if base_name == "post_de":
|
||||
self._file_path = f"{Path(__file__).parent}/mail_none.gif"
|
||||
else:
|
||||
self._file_path = f"{Path(__file__).parent}/no_deliveries_{base_name}.jpg"
|
||||
self._is_generic = True
|
||||
|
||||
if self._no_mail:
|
||||
self._file_path = self._no_mail
|
||||
_LOGGER.debug(
|
||||
"%s camera - using custom no mail: %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
return
|
||||
|
||||
image_attr_name = f"ATTR_{base_name.upper()}_IMAGE"
|
||||
image_attr = getattr(const, image_attr_name, None)
|
||||
|
||||
if not image_attr:
|
||||
return
|
||||
|
||||
required_keys = {image_attr, ATTR_IMAGE_PATH}
|
||||
if not required_keys.issubset(self.coordinator.data):
|
||||
return
|
||||
|
||||
image = self.coordinator.data[image_attr]
|
||||
image_path = self.coordinator.data[ATTR_IMAGE_PATH].rstrip("/") + "/"
|
||||
path = f"{image_path}{base_name}/"
|
||||
coordinator_file_path = f"{self.hass.config.path()}/{path}{image}"
|
||||
|
||||
_LOGGER.debug(
|
||||
"=== %s CAMERA UPDATE === coordinator %s = '%s'",
|
||||
self._type,
|
||||
image_attr,
|
||||
image,
|
||||
)
|
||||
|
||||
# Log all image-related keys in coordinator for this camera
|
||||
all_image_keys = {
|
||||
k: self.coordinator.data.get(k, "NOT SET")
|
||||
for k in self.coordinator.data
|
||||
if "image" in k.lower()
|
||||
}
|
||||
_LOGGER.debug(
|
||||
"%s camera - All image keys in coordinator: %s",
|
||||
self._type,
|
||||
all_image_keys,
|
||||
)
|
||||
|
||||
if await anyio.Path(coordinator_file_path).exists() and os.access(
|
||||
coordinator_file_path,
|
||||
os.R_OK,
|
||||
):
|
||||
self._file_path = coordinator_file_path
|
||||
_LOGGER.debug(
|
||||
"%s camera - found coordinator file: %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
else:
|
||||
await self._find_alternative_image(coordinator_file_path, image)
|
||||
|
||||
# Coordinator hashes the delivery image against the placeholder to detect
|
||||
# whether a real email image was saved; use that result rather than guessing
|
||||
# from file existence (the placeholder may have been copied to the same path).
|
||||
self._is_generic = not self.coordinator.data.get(f"{base_name}_update", False)
|
||||
|
||||
async def _find_alternative_image(
|
||||
self,
|
||||
coordinator_file_path: str,
|
||||
expected_image: str,
|
||||
) -> None:
|
||||
"""Attempt to find an alternative image in the directory."""
|
||||
path_dir = Path(coordinator_file_path).parent
|
||||
_LOGGER.debug(
|
||||
"%s camera - coordinator file not found: %s",
|
||||
self._type,
|
||||
coordinator_file_path,
|
||||
)
|
||||
|
||||
# Define a helper to run blocking I/O in the executor
|
||||
def _scan_images():
|
||||
if not path_dir.exists():
|
||||
_LOGGER.debug(
|
||||
"%s camera - directory does not exist: %s",
|
||||
self._type,
|
||||
path_dir,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
found_images = []
|
||||
for file_path in path_dir.iterdir():
|
||||
if file_path.name.lower().endswith(
|
||||
(".jpg", ".jpeg", ".png", ".gif"),
|
||||
):
|
||||
if file_path.exists() and os.access(file_path, os.R_OK):
|
||||
if "no_deliveries" not in file_path.name:
|
||||
found_images.append(
|
||||
(str(file_path), file_path.stat().st_mtime),
|
||||
)
|
||||
except OSError as err:
|
||||
_LOGGER.debug(
|
||||
"%s camera - error listing directory %s: %s",
|
||||
self._type,
|
||||
path_dir,
|
||||
err,
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return found_images
|
||||
|
||||
# Execute the scan in a background thread
|
||||
image_files = await self.hass.async_add_executor_job(_scan_images)
|
||||
|
||||
if image_files:
|
||||
image_files.sort(key=lambda x: x[1], reverse=True)
|
||||
self._file_path = image_files[0][0]
|
||||
_LOGGER.debug(
|
||||
"%s camera - found alternative image file (most recent): %s",
|
||||
self._type,
|
||||
self._file_path,
|
||||
)
|
||||
|
||||
def _is_custom_no_mail_image(self, base_name: str, file_path: str) -> bool:
|
||||
"""Check if the given file path is a custom 'no mail' image for the specified camera.
|
||||
|
||||
Args:
|
||||
base_name: The base name of the camera (e.g., 'amazon', 'ups', 'walmart', 'usps')
|
||||
file_path: The full file path to check
|
||||
|
||||
Returns:
|
||||
True if this is a custom 'no mail' image, False otherwise
|
||||
|
||||
"""
|
||||
# Handle USPS camera differently (uses CONF_CUSTOM_IMG instead of CONF_USPS_CUSTOM_IMG)
|
||||
if base_name == "usps":
|
||||
custom_img_key = "CONF_CUSTOM_IMG"
|
||||
custom_img_file_key = "CONF_CUSTOM_IMG_FILE"
|
||||
else:
|
||||
# Handle other cameras (Amazon, UPS, Walmart)
|
||||
custom_img_key = f"CONF_{base_name.upper()}_CUSTOM_IMG"
|
||||
custom_img_file_key = f"CONF_{base_name.upper()}_CUSTOM_IMG_FILE"
|
||||
|
||||
custom_img_conf = getattr(const, custom_img_key, None)
|
||||
custom_img_file_conf = getattr(const, custom_img_file_key, None)
|
||||
|
||||
if (
|
||||
custom_img_conf
|
||||
and custom_img_file_conf
|
||||
and self.config.data.get(custom_img_conf)
|
||||
):
|
||||
custom_file_path = self.config.data.get(custom_img_file_conf)
|
||||
if custom_file_path and Path(custom_file_path).exists():
|
||||
# Check if the file path matches the custom "no mail" image
|
||||
return Path(file_path).resolve() == Path(custom_file_path).resolve()
|
||||
|
||||
return False
|
||||
|
||||
def _get_sensor_name_for_camera(self, camera_type: str) -> str:
|
||||
"""Get the sensor name that corresponds to a camera type.
|
||||
|
||||
Args:
|
||||
camera_type: The camera type (e.g., 'amazon_camera', 'ups_camera', etc.)
|
||||
|
||||
Returns:
|
||||
The corresponding sensor name, or None if no mapping exists
|
||||
|
||||
"""
|
||||
# Remove "_camera" suffix to get base name
|
||||
base_name = camera_type.removesuffix("_camera")
|
||||
|
||||
# Special cases for mail scans cameras
|
||||
if base_name == "usps":
|
||||
return "usps_mail"
|
||||
if base_name == "post_de":
|
||||
return "post_de_mail"
|
||||
|
||||
# For other cameras, use the pattern: {base_name}_delivered
|
||||
return f"{base_name}_delivered"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self.coordinator.data is not None:
|
||||
await self.update_file_path()
|
||||
|
||||
async def async_on_demand_update(self):
|
||||
"""Update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
"""Return device information about the mailbox."""
|
||||
return {
|
||||
"connections": {(DOMAIN, self._unique_id)},
|
||||
"name": self._host,
|
||||
"manufacturer": "IMAP E-Mail",
|
||||
"sw_version": VERSION,
|
||||
}
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return f"camera_{self._host}_{self._type}_{self._unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the camera state attributes."""
|
||||
return {
|
||||
"file_path": self._file_path,
|
||||
"is_generic": self._is_generic,
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.data is not None
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
_LOGGER.debug(
|
||||
"%s camera - coordinator update received, updating file path",
|
||||
self._type,
|
||||
)
|
||||
self.hass.async_create_task(self._async_handle_coordinator_update())
|
||||
|
||||
async def _async_handle_coordinator_update(self) -> None:
|
||||
"""Update file path then write state so the frontend gets the correct image URL."""
|
||||
await self.update_file_path()
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,555 @@
|
||||
"""Data coordinator for Mail and Packages."""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
|
||||
import anyio
|
||||
from aioimaplib import IMAP4_SSL
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_RESOURCES,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
ConfigEntryAuthFailed,
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from . import const
|
||||
from .const import (
|
||||
ATTR_USPS_IMAGE,
|
||||
AUTH_TYPE_PASSWORD,
|
||||
CONF_ALLOW_EXTERNAL,
|
||||
CONF_AUTH_TYPE,
|
||||
CONF_CUSTOM_DAYS,
|
||||
CONF_FOLDER,
|
||||
CONF_IMAP_SECURITY,
|
||||
CONF_IMAP_TIMEOUT,
|
||||
DEFAULT_CUSTOM_DAYS,
|
||||
DEFAULT_IMAP_TIMEOUT,
|
||||
DOMAIN,
|
||||
MAX_TRACKING_AGE_DAYS,
|
||||
)
|
||||
from .helpers import copy_images
|
||||
from .shippers import get_shipper_for_sensor
|
||||
from .utils.cache import EmailCache
|
||||
from .utils.image import default_image_path, hash_file, image_file_name
|
||||
from .utils.imap import InvalidAuth, login, logout, selectfolder
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MailAndPackagesData:
|
||||
"""Data for Mail and Packages integration."""
|
||||
|
||||
coordinator: "MailDataUpdateCoordinator"
|
||||
cameras: list
|
||||
|
||||
|
||||
type MailAndPackagesConfigEntry = ConfigEntry[MailAndPackagesData]
|
||||
|
||||
|
||||
class MailDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching mail data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
config_entry: MailAndPackagesConfigEntry = None,
|
||||
):
|
||||
"""Initialize."""
|
||||
self.interval = timedelta(minutes=config.get(CONF_SCAN_INTERVAL))
|
||||
self.name = f"Mail and Packages ({config.get(CONF_HOST)})"
|
||||
self.timeout = config.get(CONF_IMAP_TIMEOUT, DEFAULT_IMAP_TIMEOUT)
|
||||
self.config = config
|
||||
self.config_entry = config_entry
|
||||
self.hass = hass
|
||||
self._data = {}
|
||||
self._file_mtime_cache = {}
|
||||
self._hash_cache = {}
|
||||
self._in_transit_tracking: dict[str, dict[str, str]] = {}
|
||||
|
||||
_LOGGER.debug("Data will be update every %s", self.interval)
|
||||
|
||||
super().__init__(hass, _LOGGER, name=self.name, update_interval=self.interval)
|
||||
|
||||
async def _get_file_hash_if_changed(self, file_path):
|
||||
"""Only hash file if mtime changed."""
|
||||
try:
|
||||
mtime = await self.hass.async_add_executor_job(os.path.getmtime, file_path)
|
||||
if (
|
||||
file_path in self._file_mtime_cache
|
||||
and self._file_mtime_cache[file_path] == mtime
|
||||
):
|
||||
return self._hash_cache.get(file_path)
|
||||
|
||||
# File changed, re-hash
|
||||
file_hash = await self.hass.async_add_executor_job(hash_file, file_path)
|
||||
self._file_mtime_cache[file_path] = mtime
|
||||
self._hash_cache[file_path] = file_hash
|
||||
except OSError:
|
||||
return None
|
||||
else:
|
||||
return file_hash
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Fetch data."""
|
||||
start = monotonic()
|
||||
try:
|
||||
async with asyncio.timeout(self.timeout):
|
||||
try:
|
||||
config = dict(self.config)
|
||||
|
||||
# Refresh OAuth2 token if using OAuth authentication
|
||||
auth_type = config.get(CONF_AUTH_TYPE, AUTH_TYPE_PASSWORD)
|
||||
if auth_type != AUTH_TYPE_PASSWORD and self.config_entry:
|
||||
try:
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
self.hass.data[DOMAIN]["oauth_provider"] = auth_type
|
||||
|
||||
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
self.hass,
|
||||
self.config_entry,
|
||||
)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(
|
||||
self.hass,
|
||||
self.config_entry,
|
||||
implementation,
|
||||
)
|
||||
await session.async_ensure_token_valid()
|
||||
config["oauth_token"] = session.token["access_token"]
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error refreshing OAuth token")
|
||||
_LOGGER.debug("OAuth token refresh error details: %s", err)
|
||||
raise UpdateFailed("OAuth token refresh failed") from err
|
||||
|
||||
data = await self.process_emails(self.hass, config)
|
||||
except UpdateFailed:
|
||||
raise
|
||||
except Exception as error:
|
||||
_LOGGER.error("Problem updating sensors: %s", error)
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
if data:
|
||||
self._data = data
|
||||
await self._binary_sensor_update()
|
||||
return self._data
|
||||
except TimeoutError:
|
||||
_LOGGER.error(
|
||||
"Mail and Packages scan exceeded its %.0fs time budget (elapsed %.1fs). "
|
||||
"This budget covers the ENTIRE scan (login plus every per-carrier IMAP "
|
||||
"search), not just connecting. Increase the scan time limit in the "
|
||||
"integration options, or reduce the mailbox size searched (use a "
|
||||
"dedicated folder), the days-back window, or the number of enabled "
|
||||
"carriers.",
|
||||
self.timeout,
|
||||
monotonic() - start,
|
||||
)
|
||||
raise
|
||||
|
||||
async def process_emails(self, hass: HomeAssistant, config: dict) -> dict:
|
||||
"""Process emails and update sensors."""
|
||||
# Initialize defaults and image paths
|
||||
data = self._initialize_data()
|
||||
config = await self._setup_image_config(hass, config)
|
||||
|
||||
# Connect to IMAP
|
||||
account = await self._get_imap_connection(config)
|
||||
try:
|
||||
cache = EmailCache(account)
|
||||
now = datetime.datetime.now()
|
||||
today = now.strftime("%d-%b-%Y")
|
||||
today_iso = now.date().isoformat()
|
||||
days = config.get(CONF_CUSTOM_DAYS, DEFAULT_CUSTOM_DAYS)
|
||||
since_date = (now - datetime.timedelta(days=days)).strftime("%d-%b-%Y")
|
||||
|
||||
# Process logic
|
||||
shipper_data = await self._update_shippers(
|
||||
account, config, today, since_date, cache
|
||||
)
|
||||
tracking_details = shipper_data.pop("_tracking_details", {})
|
||||
data.update(shipper_data)
|
||||
self._apply_tracking_state(data, tracking_details, today_iso)
|
||||
|
||||
# Aggregate global transit and delivered sensors
|
||||
self._aggregate_package_counts(data)
|
||||
finally:
|
||||
await logout(account)
|
||||
|
||||
# Post-process external images
|
||||
if config.get(CONF_ALLOW_EXTERNAL):
|
||||
try:
|
||||
await hass.async_add_executor_job(copy_images, hass, config)
|
||||
except (OSError, ValueError) as err:
|
||||
_LOGGER.error("Problem creating: %s", err)
|
||||
|
||||
return data
|
||||
|
||||
def _initialize_data(self) -> dict:
|
||||
"""Initialize core data structure with default values."""
|
||||
data = {
|
||||
"mail_updated": datetime.datetime.now(datetime.UTC).isoformat(),
|
||||
"amazon_delivered_by_others": 0,
|
||||
}
|
||||
resources = self.config.get(CONF_RESOURCES, [])
|
||||
for sensor in resources:
|
||||
if sensor not in data:
|
||||
data[sensor] = 0
|
||||
return data
|
||||
|
||||
async def _setup_image_config(self, hass: HomeAssistant, config: dict) -> dict:
|
||||
"""Configure image paths and filenames for all shippers."""
|
||||
image_path = default_image_path(hass, config)
|
||||
config["image_path"] = image_path
|
||||
|
||||
shipper_images = {
|
||||
"amazon_image": (True, False, False, False),
|
||||
"ups_image": (False, True, False, False),
|
||||
"walmart_image": (False, False, True, False),
|
||||
"fedex_image": (False, False, False, True),
|
||||
"usps_image": (False, False, False, False),
|
||||
"post_de_image": (False, False, False, False),
|
||||
}
|
||||
|
||||
for key, params in shipper_images.items():
|
||||
config[key] = await hass.async_add_executor_job(
|
||||
image_file_name, hass, config, *params
|
||||
)
|
||||
return config
|
||||
|
||||
async def _get_imap_connection(self, config: dict) -> IMAP4_SSL:
|
||||
"""Establish and return an authenticated IMAP connection."""
|
||||
try:
|
||||
account = await login(
|
||||
self.hass,
|
||||
config.get(CONF_HOST),
|
||||
config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
config.get(CONF_IMAP_SECURITY),
|
||||
config.get(CONF_VERIFY_SSL),
|
||||
config.get("oauth_token"),
|
||||
timeout=self.timeout,
|
||||
)
|
||||
except InvalidAuth as err:
|
||||
_LOGGER.error("Authentication failed: %s", err)
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error logging into IMAP: %s", err)
|
||||
raise UpdateFailed(f"Login failed: {err}") from err
|
||||
|
||||
folders = config.get(CONF_FOLDER)
|
||||
if not folders:
|
||||
folders = ["INBOX"]
|
||||
elif isinstance(folders, str):
|
||||
folders = [folders]
|
||||
elif isinstance(folders, (list, tuple, set)):
|
||||
folders = [f for f in folders if isinstance(f, str) and f]
|
||||
if not folders:
|
||||
folders = ["INBOX"]
|
||||
else:
|
||||
folders = ["INBOX"]
|
||||
account._folders = folders # noqa: SLF001
|
||||
account._current_folder = None # noqa: SLF001
|
||||
|
||||
if folders:
|
||||
try:
|
||||
folder_ok = await selectfolder(account, folders[0])
|
||||
except Exception as err:
|
||||
await logout(account)
|
||||
raise UpdateFailed(f"Folder selection failed: {err}") from err
|
||||
|
||||
if not folder_ok:
|
||||
_LOGGER.error("Error selecting folder: %s", folders[0])
|
||||
await logout(account)
|
||||
raise UpdateFailed(f"Folder selection failed: {folders[0]}")
|
||||
|
||||
return account
|
||||
|
||||
async def _update_shippers(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
config: dict,
|
||||
today: str,
|
||||
since_date: str,
|
||||
cache: EmailCache,
|
||||
) -> dict:
|
||||
"""Group and process sensors by shipper."""
|
||||
data = {}
|
||||
resources = config.get(CONF_RESOURCES, [])
|
||||
sensors_by_shipper = {}
|
||||
|
||||
for sensor in resources:
|
||||
shipper = get_shipper_for_sensor(self.hass, config, sensor)
|
||||
if shipper:
|
||||
sensors_by_shipper.setdefault(shipper.name, []).append(
|
||||
(shipper, sensor)
|
||||
)
|
||||
|
||||
for shipper_name, shipper_group in sensors_by_shipper.items():
|
||||
shipper_instance = shipper_group[0][0]
|
||||
sensors = [s[1] for s in shipper_group]
|
||||
|
||||
shipper_start = monotonic()
|
||||
success = False
|
||||
try:
|
||||
results = await shipper_instance.process_batch(
|
||||
account, today, sensors, cache, since_date=since_date
|
||||
)
|
||||
if isinstance(results, dict):
|
||||
if "_tracking_details" in results:
|
||||
data.setdefault("_tracking_details", {}).update(
|
||||
results.pop("_tracking_details")
|
||||
)
|
||||
data.update(results)
|
||||
success = True
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.error("Error processing shipper %s: %s", shipper_name, err)
|
||||
finally:
|
||||
_LOGGER.debug(
|
||||
"Shipper %s %s in %.1fs",
|
||||
shipper_name,
|
||||
"processed" if success else "failed",
|
||||
monotonic() - shipper_start,
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def _apply_tracking_state(
|
||||
self,
|
||||
data: dict,
|
||||
tracking_details: dict[str, list[str]],
|
||||
today_iso: str,
|
||||
) -> None:
|
||||
"""Update in-transit tracking state and override sensor counts."""
|
||||
prefixes: set[str] = set(self._in_transit_tracking.keys())
|
||||
for sensor_key in tracking_details:
|
||||
prefix = "_".join(sensor_key.split("_")[:-1])
|
||||
if prefix:
|
||||
prefixes.add(prefix)
|
||||
|
||||
for prefix in prefixes:
|
||||
if prefix in self._in_transit_tracking and not (
|
||||
f"{prefix}_delivering" in tracking_details
|
||||
or f"{prefix}_exception" in tracking_details
|
||||
or f"{prefix}_delivered" in tracking_details
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Prefix '%s' has no tracking_details entries — "
|
||||
"may be a removed carrier; tracking will persist until TTL expiry",
|
||||
prefix,
|
||||
)
|
||||
|
||||
delivering = list(tracking_details.get(f"{prefix}_delivering", []))
|
||||
delivering += list(tracking_details.get(f"{prefix}_exception", []))
|
||||
delivered = list(tracking_details.get(f"{prefix}_delivered", []))
|
||||
|
||||
self._update_tracking_for_prefix(
|
||||
prefix, delivering, delivered, today_iso, MAX_TRACKING_AGE_DAYS
|
||||
)
|
||||
|
||||
in_transit = self._in_transit_tracking.get(prefix, {})
|
||||
if in_transit:
|
||||
data[f"{prefix}_tracking"] = list(in_transit.keys())
|
||||
data[f"{prefix}_delivering"] = len(in_transit)
|
||||
delivered_count = data.get(f"{prefix}_delivered", 0)
|
||||
data[f"{prefix}_packages"] = len(in_transit) + (
|
||||
delivered_count if isinstance(delivered_count, int) else 0
|
||||
)
|
||||
|
||||
def _update_tracking_for_prefix(
|
||||
self,
|
||||
prefix: str,
|
||||
delivering: list[str],
|
||||
delivered: list[str],
|
||||
today_iso: str,
|
||||
ttl_days: int,
|
||||
) -> None:
|
||||
"""Add/expire delivering tracking numbers and remove delivered ones."""
|
||||
if prefix not in self._in_transit_tracking:
|
||||
self._in_transit_tracking[prefix] = {}
|
||||
|
||||
in_transit = self._in_transit_tracking[prefix]
|
||||
|
||||
# Add new delivering tracking numbers (record first-seen date)
|
||||
for tid in delivering:
|
||||
if tid and tid not in in_transit:
|
||||
in_transit[tid] = today_iso
|
||||
|
||||
# Remove delivered tracking numbers
|
||||
for tid in delivered:
|
||||
in_transit.pop(tid, None)
|
||||
|
||||
# Expire entries older than TTL
|
||||
cutoff = (
|
||||
datetime.date.fromisoformat(today_iso) - datetime.timedelta(days=ttl_days)
|
||||
).isoformat()
|
||||
expired = [tid for tid, seen in in_transit.items() if seen < cutoff]
|
||||
for tid in expired:
|
||||
del in_transit[tid]
|
||||
|
||||
def _aggregate_package_counts(self, data: dict) -> None:
|
||||
"""Aggregate global transit and delivered counts from all shippers."""
|
||||
# Only update if sensors were requested in initialize_data
|
||||
if "zpackages_transit" in data:
|
||||
data["zpackages_transit"] = self._sum_transit_counts(data)
|
||||
if "zpackages_delivered" in data:
|
||||
data["zpackages_delivered"] = self._sum_delivered_counts(data)
|
||||
|
||||
def _sum_delivered_counts(self, data: dict) -> int:
|
||||
"""Sum delivered packages from all shippers."""
|
||||
delivered = 0
|
||||
exclude_keys = (
|
||||
"zpackages_delivered",
|
||||
"amazon_delivered_by_others",
|
||||
"usps_mail_delivered",
|
||||
)
|
||||
|
||||
for key, value in data.items():
|
||||
if (
|
||||
isinstance(value, int)
|
||||
and value > 0
|
||||
and key.endswith("_delivered")
|
||||
and key not in exclude_keys
|
||||
):
|
||||
delivered += value
|
||||
return delivered
|
||||
|
||||
def _sum_transit_counts(self, data: dict) -> int:
|
||||
"""Sum transit and exception packages from all shippers."""
|
||||
transit = 0
|
||||
shippers_counted = set()
|
||||
|
||||
# Amazon is special as it uses amazon_packages for total arriving
|
||||
if data.get("amazon_packages", 0) > 0:
|
||||
transit += data["amazon_packages"]
|
||||
shippers_counted.add("amazon")
|
||||
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, int) or value <= 0:
|
||||
continue
|
||||
|
||||
# Add exceptions for all shippers
|
||||
if key.endswith("_exception") and key != "zpackages_exception":
|
||||
transit += value
|
||||
continue
|
||||
|
||||
# Match shipper prefix
|
||||
shipper = next((s for s in const.SHIPPERS if key.startswith(s)), None)
|
||||
if not shipper or shipper in shippers_counted:
|
||||
continue
|
||||
|
||||
# Priority: _delivering (preferred generic state) or _packages
|
||||
if key.endswith(("_delivering", "_packages")):
|
||||
transit += value
|
||||
shippers_counted.add(shipper)
|
||||
|
||||
return transit
|
||||
|
||||
async def _binary_sensor_update(self): # noqa: C901
|
||||
"""Update binary sensor states."""
|
||||
# USPS uses ATTR_USPS_IMAGE instead of the old ATTR_IMAGE_NAME
|
||||
_LOGGER.debug("Data: %s", self._data)
|
||||
image = self._data.get(ATTR_USPS_IMAGE)
|
||||
if image:
|
||||
path = default_image_path(self.hass, self.config)
|
||||
usps_image = f"{path}/{image}"
|
||||
usps_none = f"{Path(__file__).parent}/mail_none.gif"
|
||||
usps_check = await anyio.Path(usps_image).exists()
|
||||
_LOGGER.debug("USPS Check: %s", usps_check)
|
||||
if usps_check:
|
||||
# Optimized: Use _get_file_hash_if_changed
|
||||
image_hash = await self._get_file_hash_if_changed(usps_image)
|
||||
none_hash = await self._get_file_hash_if_changed(usps_none)
|
||||
|
||||
_LOGGER.debug("USPS Image hash: %s", image_hash)
|
||||
_LOGGER.debug("USPS None hash: %s", none_hash)
|
||||
|
||||
if image_hash != none_hash:
|
||||
self._data["usps_update"] = True
|
||||
else:
|
||||
self._data["usps_update"] = False
|
||||
|
||||
# Handle generic delivery cameras (Amazon, UPS, Walmart, FedEx, Generic) with unified logic
|
||||
# Derive camera list dynamically from CAMERA_DATA, excluding usps_camera and generic_camera
|
||||
delivery_cameras = [
|
||||
camera_type.replace("_camera", "")
|
||||
for camera_type in const.CAMERA_DATA
|
||||
if camera_type not in ("usps_camera", "generic_camera")
|
||||
]
|
||||
|
||||
for base_name in delivery_cameras:
|
||||
# Derive attribute and config keys dynamically
|
||||
image_attr_name = f"ATTR_{base_name.upper()}_IMAGE"
|
||||
image_attr = getattr(const, image_attr_name, None)
|
||||
if not image_attr:
|
||||
continue
|
||||
|
||||
custom_img_key = getattr(
|
||||
const,
|
||||
f"CONF_{base_name.upper()}_CUSTOM_IMG",
|
||||
None,
|
||||
)
|
||||
custom_img_file_key = getattr(
|
||||
const,
|
||||
f"CONF_{base_name.upper()}_CUSTOM_IMG_FILE",
|
||||
None,
|
||||
)
|
||||
update_key = f"{base_name}_update"
|
||||
|
||||
image = self._data.get(image_attr)
|
||||
_LOGGER.debug("%s image from data: %s", base_name.title(), image)
|
||||
if image:
|
||||
# Normalize path to avoid double slashes
|
||||
image_path = (
|
||||
default_image_path(self.hass, self.config).rstrip("/") + "/"
|
||||
)
|
||||
path = f"{image_path}{base_name}/"
|
||||
# Use absolute path for file existence check
|
||||
delivery_image_relative = f"{path}{image}"
|
||||
delivery_image = f"{self.hass.config.path()}/{delivery_image_relative}"
|
||||
_LOGGER.debug(
|
||||
"Full %s image path: %s",
|
||||
base_name.title(),
|
||||
delivery_image,
|
||||
)
|
||||
|
||||
if custom_img_key and self.config.get(custom_img_key):
|
||||
none_image = self.config.get(custom_img_file_key)
|
||||
elif base_name == "post_de":
|
||||
none_image = f"{Path(__file__).parent}/mail_none.gif"
|
||||
else:
|
||||
none_image = (
|
||||
f"{Path(__file__).parent}/no_deliveries_{base_name}.jpg"
|
||||
)
|
||||
|
||||
image_check = await anyio.Path(delivery_image).exists()
|
||||
_LOGGER.debug("%s Check: %s", base_name.title(), image_check)
|
||||
if image_check:
|
||||
# Optimized: Use _get_file_hash_if_changed
|
||||
image_hash = await self._get_file_hash_if_changed(delivery_image)
|
||||
none_hash = await self._get_file_hash_if_changed(none_image)
|
||||
|
||||
_LOGGER.debug("%s Image hash: %s", base_name.title(), image_hash)
|
||||
_LOGGER.debug("%s None hash: %s", base_name.title(), none_hash)
|
||||
|
||||
if image_hash != none_hash:
|
||||
self._data[update_key] = True
|
||||
else:
|
||||
self._data[update_key] = False
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Provide diagnostics for Mail and Packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from . import MailAndPackagesConfigEntry
|
||||
from .const import CONF_AMAZON_FWDS, CONF_FORWARDED_EMAILS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REDACT_KEYS = {CONF_PASSWORD, CONF_USERNAME, CONF_AMAZON_FWDS, CONF_FORWARDED_EMAILS}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MailAndPackagesConfigEntry, # pylint: disable=unused-argument
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
diag: dict[str, Any] = {}
|
||||
diag["config"] = config_entry.as_dict()
|
||||
return async_redact_data(diag, REDACT_KEYS)
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MailAndPackagesConfigEntry,
|
||||
device: DeviceEntry, # pylint: disable=unused-argument
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
dynamic_keys = {
|
||||
variable
|
||||
for variable in coordinator.data
|
||||
if "tracking" in variable or "order" in variable
|
||||
}
|
||||
redact_keys = REDACT_KEYS | dynamic_keys
|
||||
|
||||
_LOGGER.debug("Redacted keys: %s", redact_keys)
|
||||
|
||||
return async_redact_data(coordinator.data, redact_keys)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Support for Mail and Packages entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntityDescription
|
||||
|
||||
|
||||
@dataclass
|
||||
class MailandPackagesBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Class describing Mail and Packages binary sensor entities."""
|
||||
|
||||
selectable: bool = False
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Helper functions for Mail and Packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
from typing import Any
|
||||
|
||||
from aioimaplib import IMAP4_SSL
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
AMAZON_DELIVERED_SUBJECT,
|
||||
AMAZON_EXCEPTION_SUBJECT,
|
||||
AMAZON_HUB_SUBJECT,
|
||||
AMAZON_OTP_SUBJECT,
|
||||
ATTR_COUNT,
|
||||
ATTR_TRACKING,
|
||||
BINARY_SENSORS,
|
||||
CONF_PATH,
|
||||
SENSOR_TYPES,
|
||||
)
|
||||
from .shippers import SHIPPER_REGISTRY
|
||||
from .shippers.usps import USPSShipper
|
||||
from .utils.image import (
|
||||
default_image_path,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Legacy constants for tests
|
||||
|
||||
amazon_exception = AMAZON_EXCEPTION_SUBJECT
|
||||
amazon_hub = AMAZON_HUB_SUBJECT
|
||||
amazon_otp = AMAZON_OTP_SUBJECT
|
||||
amazon_search_legacy = AMAZON_DELIVERED_SUBJECT
|
||||
image_file_name = "mail_today.gif"
|
||||
|
||||
|
||||
async def get_count(
|
||||
account: IMAP4_SSL,
|
||||
sensor_type: str,
|
||||
body_count: bool,
|
||||
image_path: str,
|
||||
hass: HomeAssistant,
|
||||
) -> dict[str, Any]:
|
||||
"""Legacy get_count wrapper for tests."""
|
||||
shipper_class = SHIPPER_REGISTRY.get(sensor_type)
|
||||
if not shipper_class:
|
||||
_LOGGER.error("No shipper found for %s", sensor_type)
|
||||
return {ATTR_COUNT: 0, ATTR_TRACKING: []}
|
||||
|
||||
shipper = shipper_class(hass, {CONF_PATH: image_path})
|
||||
today = datetime.datetime.now().strftime("%d-%b-%Y")
|
||||
return await shipper.process(account, today, sensor_type)
|
||||
|
||||
|
||||
async def get_mails(
|
||||
account: IMAP4_SSL,
|
||||
address_list: list[str],
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
) -> int:
|
||||
"""Legacy get_mails wrapper for tests."""
|
||||
shipper = USPSShipper(hass, config)
|
||||
today = datetime.datetime.now().strftime("%d-%b-%Y")
|
||||
result = await shipper.process(account, today, "usps_mail")
|
||||
return result.get(ATTR_COUNT, 0)
|
||||
|
||||
|
||||
async def fetch(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
account: IMAP4_SSL,
|
||||
sensor: str,
|
||||
) -> int:
|
||||
"""Legacy fetch wrapper for tests."""
|
||||
shipper_class = SHIPPER_REGISTRY.get(sensor)
|
||||
if not shipper_class:
|
||||
return 0
|
||||
|
||||
shipper = shipper_class(hass, config)
|
||||
today = datetime.datetime.now().strftime("%d-%b-%Y")
|
||||
data = await shipper.process(account, today, sensor)
|
||||
return data.get(ATTR_COUNT, 0)
|
||||
|
||||
|
||||
async def get_items(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
account: IMAP4_SSL,
|
||||
sensor: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Legacy get_items wrapper for tests."""
|
||||
shipper_class = SHIPPER_REGISTRY.get(sensor)
|
||||
if not shipper_class:
|
||||
return {ATTR_COUNT: 0, ATTR_TRACKING: []}
|
||||
|
||||
shipper = shipper_class(hass, config)
|
||||
today = datetime.datetime.now().strftime("%d-%b-%Y")
|
||||
return await shipper.process(account, today, sensor)
|
||||
|
||||
|
||||
def get_resources(hass: HomeAssistant | None = None) -> dict:
|
||||
"""Return resources from const."""
|
||||
resources = {k: v.name for k, v in SENSOR_TYPES.items()}
|
||||
resources.update({k: v.name for k, v in BINARY_SENSORS.items() if v.selectable})
|
||||
return resources
|
||||
|
||||
|
||||
def copy_images(hass: HomeAssistant, config: ConfigEntry) -> None:
|
||||
"""Copy processed images to www directory."""
|
||||
image_path = Path(hass.config.path()) / default_image_path(hass, config)
|
||||
www_path = Path(hass.config.path()) / "www" / "mail_and_packages"
|
||||
|
||||
if not www_path.is_dir():
|
||||
www_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for root, _, files in os.walk(image_path):
|
||||
for file in files:
|
||||
if file.endswith((".gif", ".jpg", ".png")):
|
||||
src = Path(root) / file
|
||||
dest = www_path / file
|
||||
try:
|
||||
copyfile(str(src), str(dest))
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error copying image: %s", err)
|
||||
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1 @@
|
||||
# Placeholder to keep default images directory
|
||||
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"domain": "mail_and_packages",
|
||||
"name": "Mail and Packages",
|
||||
"after_dependencies": ["cloud"],
|
||||
"codeowners": [
|
||||
"@moralmunky",
|
||||
"@firstof9"
|
||||
],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://github.com/moralmunky/Home-Assistant-Mail-And-Packages",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/issues",
|
||||
"requirements": [
|
||||
"beautifulsoup4",
|
||||
"Pillow>=9.0",
|
||||
"dateparser",
|
||||
"aioimaplib"
|
||||
],
|
||||
"version": "0.5.9"
|
||||
}
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,289 @@
|
||||
"""Based on @skalavala work.
|
||||
|
||||
https://blog.kalavala.net/usps/homeassistant/mqtt/2018/01/12/usps.html
|
||||
Configuration code contribution from @firstof9 https://github.com/firstof9/
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_RESOURCES
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import MailAndPackagesConfigEntry
|
||||
from .const import (
|
||||
AMAZON_EXCEPTION,
|
||||
AMAZON_EXCEPTION_ORDER,
|
||||
AMAZON_HUB,
|
||||
AMAZON_HUB_CODE,
|
||||
AMAZON_ORDER,
|
||||
AMAZON_OTP,
|
||||
AMAZON_OTP_CODE,
|
||||
ATTR_CODE,
|
||||
ATTR_GRID_IMAGE_NAME,
|
||||
ATTR_IMAGE,
|
||||
ATTR_IMAGE_NAME,
|
||||
ATTR_IMAGE_PATH,
|
||||
ATTR_ORDER,
|
||||
ATTR_TRACKING_NUM,
|
||||
ATTR_USPS_IMAGE,
|
||||
CONF_PATH,
|
||||
DOMAIN,
|
||||
IMAGE_SENSORS,
|
||||
SENSOR_TYPES,
|
||||
VERSION,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DELIVERED_SUFFIXES = {"delivered"}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass,
|
||||
entry: MailAndPackagesConfigEntry,
|
||||
async_add_entities,
|
||||
):
|
||||
"""Set up the sensor entities."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
resources = entry.data.get(CONF_RESOURCES, [])
|
||||
|
||||
sensors = [
|
||||
PackagesSensor(entry, SENSOR_TYPES[variable], coordinator)
|
||||
for variable in resources
|
||||
if variable in SENSOR_TYPES
|
||||
]
|
||||
|
||||
sensors.extend(
|
||||
ImagePathSensors(hass, entry, value, coordinator)
|
||||
for value in IMAGE_SENSORS.values()
|
||||
)
|
||||
|
||||
async_add_entities(sensors, False)
|
||||
|
||||
|
||||
class PackagesSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Representation of a sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: ConfigEntry,
|
||||
sensor_description: SensorEntityDescription,
|
||||
coordinator: Any,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = sensor_description
|
||||
self.coordinator = coordinator
|
||||
self._config = config
|
||||
self._name = sensor_description.name
|
||||
self.type = sensor_description.key
|
||||
self._host = config.data[CONF_HOST]
|
||||
self._unique_id = self._config.entry_id
|
||||
self.data = self.coordinator.data
|
||||
parts = self.type.split("_")
|
||||
if len(parts) > 1:
|
||||
prefix = "_".join(parts[:-1])
|
||||
if parts[-1] in DELIVERED_SUFFIXES:
|
||||
self._tracking_key = f"{prefix}_delivered_tracking"
|
||||
else:
|
||||
self._tracking_key = f"{prefix}_tracking"
|
||||
else:
|
||||
self._tracking_key = f"{self.type}_tracking"
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
"""Return device information about the mailbox."""
|
||||
return {
|
||||
"connections": {(DOMAIN, self._unique_id)},
|
||||
"name": self._host,
|
||||
"manufacturer": "IMAP E-Mail",
|
||||
"sw_version": VERSION,
|
||||
}
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return f"sensor_{self._host}_{self.type}_{self._unique_id}"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_value(self) -> Any:
|
||||
"""Return the state of the sensor."""
|
||||
value = self.coordinator.data.get(self.type)
|
||||
|
||||
if self.type == "mail_updated":
|
||||
# Safely handle string vs datetime to prevent ValueError
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value = datetime.datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
value = datetime.datetime.now(datetime.UTC)
|
||||
elif value is None:
|
||||
value = datetime.datetime.now(datetime.UTC)
|
||||
return value
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No need to poll. Coordinator notifies entity of updates."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.data is not None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> str | None:
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
data = self.coordinator.data
|
||||
|
||||
if any(
|
||||
sensor in self.type
|
||||
for sensor in ["_delivering", "_delivered", "_packages", "_exception"]
|
||||
):
|
||||
if tracking := data.get(self._tracking_key):
|
||||
attr[ATTR_TRACKING_NUM] = tracking
|
||||
|
||||
# Catch no data entries
|
||||
if self.data is None:
|
||||
return attr
|
||||
|
||||
if "Amazon" in self._name:
|
||||
self._add_amazon_attributes(attr, data)
|
||||
elif self._name == "Mail USPS Mail":
|
||||
if image_name := data.get(ATTR_IMAGE_NAME):
|
||||
attr[ATTR_IMAGE] = image_name
|
||||
|
||||
return attr
|
||||
|
||||
def _add_amazon_attributes(self, attr: dict, data: dict) -> None:
|
||||
"""Add Amazon specific attributes to the sensor."""
|
||||
if self.type == AMAZON_EXCEPTION:
|
||||
if order := data.get(AMAZON_EXCEPTION_ORDER, data.get(ATTR_ORDER)):
|
||||
attr[ATTR_ORDER] = order
|
||||
elif order := data.get(AMAZON_ORDER):
|
||||
attr[ATTR_ORDER] = order
|
||||
elif self.type == AMAZON_HUB:
|
||||
if code := data.get(AMAZON_HUB_CODE, data.get(ATTR_CODE)):
|
||||
attr[ATTR_CODE] = code
|
||||
elif self.type == AMAZON_OTP:
|
||||
if code := data.get(AMAZON_OTP_CODE, data.get(ATTR_CODE)):
|
||||
attr[ATTR_CODE] = code
|
||||
|
||||
|
||||
class ImagePathSensors(CoordinatorEntity, SensorEntity):
|
||||
"""Representation of a sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
sensor_description: SensorEntityDescription,
|
||||
coordinator: Any,
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = sensor_description
|
||||
self.hass = hass
|
||||
self.coordinator = coordinator
|
||||
self._config = config
|
||||
self._name = sensor_description.name
|
||||
self.type = sensor_description.key
|
||||
self._host = config.data[CONF_HOST]
|
||||
self._unique_id = self._config.entry_id
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict:
|
||||
"""Return device information about the mailbox."""
|
||||
return {
|
||||
"connections": {(DOMAIN, self._unique_id)},
|
||||
"name": self._host,
|
||||
"manufacturer": "IMAP E-Mail",
|
||||
"sw_version": VERSION,
|
||||
}
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return f"sensor_{self._host}_{self.type}_{self._unique_id}"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
image = ""
|
||||
the_path = None
|
||||
|
||||
image = self.coordinator.data.get(ATTR_USPS_IMAGE)
|
||||
|
||||
grid_image = self.coordinator.data.get(ATTR_GRID_IMAGE_NAME)
|
||||
|
||||
path = self.coordinator.data.get(
|
||||
ATTR_IMAGE_PATH,
|
||||
self._config.data.get(CONF_PATH),
|
||||
)
|
||||
|
||||
if self.type == "usps_mail_image_system_path" and image:
|
||||
_LOGGER.debug("Updating system image path to: %s", path)
|
||||
the_path = f"{self.hass.config.path()}/{path}{image}"
|
||||
elif self.type == "usps_mail_grid_image_path" and grid_image:
|
||||
_LOGGER.debug("Updating grid image path to: %s", path)
|
||||
the_path = f"{self.hass.config.path()}/{path}{grid_image}"
|
||||
elif self.type == "usps_mail_image_url" and image:
|
||||
url = self._get_base_url()
|
||||
if url:
|
||||
the_path = f"{url.rstrip('/')}/local/mail_and_packages/{image}"
|
||||
return the_path
|
||||
|
||||
def _get_base_url(self) -> str | None:
|
||||
"""Return the best available base URL for building image links.
|
||||
|
||||
Priority: explicit external URL → HA Cloud remote URL → internal URL.
|
||||
"""
|
||||
if self.hass.config.external_url:
|
||||
return self.hass.config.external_url
|
||||
|
||||
# Try Home Assistant Cloud (Nabu Casa) — its remote URL is not exposed via
|
||||
# hass.config.external_url when "Use Home Assistant Cloud" is selected.
|
||||
try:
|
||||
from homeassistant.components.cloud import ( # noqa: PLC0415
|
||||
CloudNotAvailable,
|
||||
async_remote_ui_url,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
return async_remote_ui_url(self.hass)
|
||||
except (CloudNotAvailable, KeyError):
|
||||
_LOGGER.debug("HA Cloud remote URL not available.")
|
||||
|
||||
if self.hass.config.internal_url:
|
||||
_LOGGER.debug("Falling back to internal URL for image link.")
|
||||
return self.hass.config.internal_url
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No need to poll. Coordinator notifies entity of updates."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self.coordinator.data is not None
|
||||
@@ -0,0 +1,13 @@
|
||||
update_image:
|
||||
name: Refresh the Mail Camera(s)
|
||||
description: Refreshes the Mail Camera specified or all of them at once. Leave blank to refresh them all.
|
||||
fields:
|
||||
entity_id:
|
||||
name: Camera Entity
|
||||
description: The camera entity to refresh.
|
||||
example: camera.mail_usps_camera
|
||||
required: false
|
||||
selector:
|
||||
entity:
|
||||
domain: camera
|
||||
integration: mail_and_packages
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Shippers for Mail and Packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .amazon import AmazonShipper
|
||||
from .generic import GenericShipper
|
||||
from .post_de import PostDEShipper
|
||||
from .usps import USPSShipper
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .base import Shipper
|
||||
|
||||
SHIPPER_REGISTRY = {
|
||||
"amazon": AmazonShipper,
|
||||
"generic": GenericShipper,
|
||||
"post_de": PostDEShipper,
|
||||
"usps": USPSShipper,
|
||||
}
|
||||
|
||||
|
||||
def get_shipper_for_sensor(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
sensor_type: str,
|
||||
) -> Shipper | None:
|
||||
"""Return the appropriate shipper for the given sensor type."""
|
||||
# Check specialized shippers first
|
||||
for name, shipper_class in SHIPPER_REGISTRY.items():
|
||||
if name == "generic":
|
||||
continue
|
||||
if shipper_class.handles_sensor(sensor_type):
|
||||
return shipper_class(hass, config)
|
||||
|
||||
# Fallback to generic
|
||||
if GenericShipper.handles_sensor(sensor_type):
|
||||
return GenericShipper(hass, config)
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,612 @@
|
||||
"""Base Shipper class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import email
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import dateparser
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from aioimaplib import IMAP4_SSL
|
||||
|
||||
from custom_components.mail_and_packages import const
|
||||
from custom_components.mail_and_packages.const import (
|
||||
AMAZON_DELIVERED,
|
||||
AMAZON_DELIVERED_SUBJECT,
|
||||
AMAZON_EXCEPTION,
|
||||
AMAZON_EXCEPTION_BODY,
|
||||
AMAZON_EXCEPTION_ORDER,
|
||||
AMAZON_EXCEPTION_SUBJECT,
|
||||
AMAZON_HUB,
|
||||
AMAZON_HUB_BODY,
|
||||
AMAZON_HUB_CODE,
|
||||
AMAZON_HUB_SUBJECT,
|
||||
AMAZON_HUB_SUBJECT_SEARCH,
|
||||
AMAZON_ORDER,
|
||||
AMAZON_ORDERED_SUBJECT,
|
||||
AMAZON_OTP,
|
||||
AMAZON_OTP_CODE,
|
||||
AMAZON_OTP_REGEX,
|
||||
AMAZON_OTP_SUBJECT,
|
||||
AMAZON_PACKAGES,
|
||||
AMAZON_SHIPMENT_SUBJECT,
|
||||
ATTR_COUNT,
|
||||
CONF_AMAZON_DAYS,
|
||||
CONF_AMAZON_DOMAIN,
|
||||
CONF_AMAZON_FWDS,
|
||||
CONF_DURATION,
|
||||
CONF_FORWARDING_HEADER,
|
||||
DEFAULT_AMAZON_DAYS,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.amazon import (
|
||||
_extract_hub_code,
|
||||
amazon_email_addresses,
|
||||
download_amazon_img,
|
||||
extract_order_numbers,
|
||||
filter_amazon_strings,
|
||||
get_decoded_subject,
|
||||
get_email_body,
|
||||
parse_amazon_arrival_date,
|
||||
search_amazon_emails,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
from custom_components.mail_and_packages.utils.date import get_today
|
||||
from custom_components.mail_and_packages.utils.image import (
|
||||
cleanup_images,
|
||||
generate_delivery_gif,
|
||||
random_filename,
|
||||
resize_images,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.imap import (
|
||||
email_fetch,
|
||||
email_search,
|
||||
)
|
||||
|
||||
from .base import Shipper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AmazonShipper(Shipper):
|
||||
"""Amazon shipper implementation."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return shipper name."""
|
||||
return "amazon"
|
||||
|
||||
@classmethod
|
||||
def handles_sensor(cls, sensor_type: str) -> bool:
|
||||
"""Return True if this shipper handles the given sensor type."""
|
||||
return sensor_type.startswith("amazon_") or sensor_type == AMAZON_PACKAGES
|
||||
|
||||
async def process(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensor_type: str,
|
||||
cache: EmailCache | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process Amazon-specific emails."""
|
||||
forwarding_header = self.config.get(CONF_FORWARDING_HEADER, "")
|
||||
if forwarding_header and forwarding_header != "(none)":
|
||||
# Header mode: use native Amazon addresses; fwds not needed
|
||||
fwds = None
|
||||
else:
|
||||
forwarding_header = ""
|
||||
fwds = cv.ensure_list_csv(self.config.get(CONF_AMAZON_FWDS))
|
||||
days = self.config.get(CONF_AMAZON_DAYS, DEFAULT_AMAZON_DAYS)
|
||||
domain = self.config.get(CONF_AMAZON_DOMAIN)
|
||||
|
||||
if sensor_type in [AMAZON_PACKAGES, AMAZON_ORDER]:
|
||||
param = "count" if sensor_type == AMAZON_PACKAGES else "order"
|
||||
result = await self._parse_amazon_emails(
|
||||
account, param, fwds, days, domain, cache, forwarding_header
|
||||
)
|
||||
return {sensor_type: result}
|
||||
|
||||
if sensor_type == AMAZON_HUB:
|
||||
return await self._amazon_hub(account, fwds, cache, forwarding_header)
|
||||
|
||||
if sensor_type == AMAZON_OTP:
|
||||
result = await self._amazon_otp(account, fwds, cache, forwarding_header)
|
||||
return {sensor_type: result}
|
||||
|
||||
if sensor_type == AMAZON_EXCEPTION:
|
||||
return await self._amazon_exception(
|
||||
account, fwds, domain, cache, forwarding_header
|
||||
)
|
||||
|
||||
if sensor_type == AMAZON_DELIVERED:
|
||||
image_path = self.config.get("image_path")
|
||||
image_name = self.config.get("amazon_image")
|
||||
result = await self._amazon_search(
|
||||
account,
|
||||
image_path,
|
||||
image_name,
|
||||
domain,
|
||||
fwds,
|
||||
cache,
|
||||
forwarding_header,
|
||||
)
|
||||
return {
|
||||
sensor_type: result,
|
||||
const.ATTR_AMAZON_IMAGE: image_name,
|
||||
const.ATTR_IMAGE_PATH: image_path,
|
||||
}
|
||||
|
||||
return {ATTR_COUNT: 0}
|
||||
|
||||
async def process_batch(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensors: list[str],
|
||||
cache: EmailCache,
|
||||
since_date: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process multiple Amazon sensors in batch."""
|
||||
res = {}
|
||||
for sensor in sensors:
|
||||
sensor_res = await self.process(account, date, sensor, cache)
|
||||
res.update(sensor_res)
|
||||
# Replicate coordinator dictionary logic
|
||||
if sensor not in sensor_res:
|
||||
if ATTR_COUNT in sensor_res:
|
||||
res[sensor] = sensor_res[ATTR_COUNT]
|
||||
return res
|
||||
|
||||
# Internal helper methods (migrated from helpers.py)
|
||||
|
||||
async def _parse_amazon_emails(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
param: str,
|
||||
fwds: list[str] | None = None,
|
||||
days: int = DEFAULT_AMAZON_DAYS,
|
||||
domain: str | None = None,
|
||||
cache: EmailCache | None = None,
|
||||
forwarding_header: str = "",
|
||||
) -> list[str] | int:
|
||||
"""Parse Amazon emails for delivery date and order number."""
|
||||
today_date = get_today()
|
||||
address_list = amazon_email_addresses(fwds, domain)
|
||||
unique_emails = await search_amazon_emails(
|
||||
account, address_list, days, domain, cache, forwarding_header
|
||||
)
|
||||
order_pattern = re.compile(r"[0-9]{3}-[0-9]{7}-[0-9]{7}")
|
||||
|
||||
context = {
|
||||
"today": today_date,
|
||||
"packages_arriving_today": {},
|
||||
"delivered_packages": {},
|
||||
"amazon_delivered": [],
|
||||
"deliveries_today": [],
|
||||
"all_shipped_orders": set(),
|
||||
"order_pattern": order_pattern,
|
||||
}
|
||||
|
||||
for email_id in unique_emails:
|
||||
await self._process_amazon_email(account, email_id, context, cache)
|
||||
|
||||
final_count = self._calculate_final_count(context)
|
||||
|
||||
if param == "count":
|
||||
return final_count
|
||||
return list(context["all_shipped_orders"])
|
||||
|
||||
async def _process_amazon_email(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
email_id: bytes | str,
|
||||
ctx: dict,
|
||||
cache: EmailCache | None = None,
|
||||
):
|
||||
"""Process a single Amazon email."""
|
||||
fetch_id = email_id.decode() if isinstance(email_id, bytes) else email_id
|
||||
if cache:
|
||||
data = (await cache.fetch(fetch_id, "(RFC822)"))[1]
|
||||
else:
|
||||
data = (await email_fetch(account, fetch_id, "(RFC822)"))[1]
|
||||
|
||||
for response_part in data:
|
||||
if not isinstance(response_part, (bytes, bytearray)):
|
||||
continue
|
||||
|
||||
msg = email.message_from_bytes(response_part)
|
||||
email_date = await self._parse_email_date(msg)
|
||||
email_subject = get_decoded_subject(msg)
|
||||
|
||||
if any(s.lower() in email_subject.lower() for s in AMAZON_ORDERED_SUBJECT):
|
||||
continue
|
||||
|
||||
email_msg = get_email_body(msg)
|
||||
if any(
|
||||
s.lower() in email_subject.lower() for s in AMAZON_DELIVERED_SUBJECT
|
||||
):
|
||||
self._handle_delivered_email(email_subject, email_msg, ctx)
|
||||
continue
|
||||
|
||||
await self._handle_shipping_email(email_subject, email_msg, email_date, ctx)
|
||||
|
||||
async def _parse_email_date(
|
||||
self,
|
||||
msg: email.message.Message,
|
||||
) -> datetime.date | None:
|
||||
"""Parse the date from an email message."""
|
||||
date_str = msg.get("Date")
|
||||
if not date_str:
|
||||
return None
|
||||
parsed = await self.hass.async_add_executor_job(dateparser.parse, date_str)
|
||||
return parsed.date() if parsed else None
|
||||
|
||||
def _handle_delivered_email(self, subject: str, body: str | None, ctx: dict):
|
||||
"""Handle an Amazon 'delivered' email."""
|
||||
orders = extract_order_numbers(subject, ctx["order_pattern"])
|
||||
if not orders and body:
|
||||
orders = extract_order_numbers(body, ctx["order_pattern"])
|
||||
for o in orders:
|
||||
ctx["delivered_packages"][o] = ctx["delivered_packages"].get(o, 0) + 1
|
||||
if o not in ctx["amazon_delivered"]:
|
||||
ctx["amazon_delivered"].append(o)
|
||||
|
||||
async def _handle_shipping_email(
|
||||
self,
|
||||
subject: str,
|
||||
body: str | None,
|
||||
date: datetime.date | None,
|
||||
ctx: dict,
|
||||
):
|
||||
"""Handle an Amazon 'shipping' or 'arriving' email."""
|
||||
order_id = self._extract_first_order_id(subject, body, ctx["order_pattern"])
|
||||
if order_id:
|
||||
ctx["all_shipped_orders"].add(order_id)
|
||||
|
||||
if body:
|
||||
parsed_arrival = await parse_amazon_arrival_date(self.hass, body, date)
|
||||
if parsed_arrival == ctx["today"]:
|
||||
if order_id:
|
||||
ctx["packages_arriving_today"][order_id] = (
|
||||
ctx["packages_arriving_today"].get(order_id, 0) + 1
|
||||
)
|
||||
else:
|
||||
ctx["deliveries_today"].append("Amazon Order")
|
||||
|
||||
def _extract_first_order_id(
|
||||
self,
|
||||
subject: str,
|
||||
body: str | None,
|
||||
pattern: re.Pattern,
|
||||
) -> str | None:
|
||||
"""Extract the first order number found in subject or body."""
|
||||
orders = extract_order_numbers(subject, pattern)
|
||||
if orders:
|
||||
return orders[0]
|
||||
if body:
|
||||
orders = extract_order_numbers(body, pattern)
|
||||
if orders:
|
||||
return orders[0]
|
||||
return None
|
||||
|
||||
def _calculate_final_count(self, ctx: dict) -> int:
|
||||
"""Calculate the final count of packages arriving today."""
|
||||
deliveries_today = [
|
||||
item
|
||||
for item in ctx["deliveries_today"]
|
||||
if item not in ctx["amazon_delivered"]
|
||||
]
|
||||
final_count = 0
|
||||
for order_id, arriving_count in ctx["packages_arriving_today"].items():
|
||||
delivered_count = ctx["delivered_packages"].get(order_id, 0)
|
||||
final_count += max(0, arriving_count - delivered_count)
|
||||
return final_count + len(deliveries_today)
|
||||
|
||||
async def _amazon_search(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
image_path: str,
|
||||
amazon_image_name: str,
|
||||
amazon_domain: str,
|
||||
fwds: list[str] | None = None,
|
||||
cache: EmailCache | None = None,
|
||||
forwarding_header: str = "",
|
||||
) -> int:
|
||||
"""Find Amazon Delivered email and handle images."""
|
||||
_LOGGER.debug("=== AMAZON DELIVERED SEARCH START ===")
|
||||
subjects = AMAZON_DELIVERED_SUBJECT
|
||||
today = get_today().strftime("%d-%b-%Y")
|
||||
count = 0
|
||||
all_image_urls = []
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
cleanup_images,
|
||||
f"{image_path or ''}amazon/",
|
||||
)
|
||||
|
||||
address_list = amazon_email_addresses(fwds, amazon_domain)
|
||||
_LOGGER.debug("Amazon email search addresses: %s", address_list)
|
||||
if amazon_domain:
|
||||
subjects = filter_amazon_strings(subjects, amazon_domain)
|
||||
|
||||
(server_response, data) = await email_search(
|
||||
account=account,
|
||||
address=address_list,
|
||||
date=today,
|
||||
subject=subjects,
|
||||
header=forwarding_header,
|
||||
)
|
||||
if server_response == "OK" and data[0]:
|
||||
for email_id in data[0].split():
|
||||
fetch_id = (
|
||||
email_id.decode() if isinstance(email_id, bytes) else email_id
|
||||
)
|
||||
if cache:
|
||||
msg_data = (await cache.fetch(fetch_id, "(RFC822)"))[1]
|
||||
else:
|
||||
msg_data = (await email_fetch(account, fetch_id, "(RFC822)"))[1]
|
||||
|
||||
is_delivered, urls = self._is_amazon_delivered(msg_data, subjects)
|
||||
if is_delivered:
|
||||
count += 1
|
||||
for url in urls:
|
||||
if url not in all_image_urls:
|
||||
all_image_urls.append(url)
|
||||
|
||||
await self._process_amazon_images(
|
||||
all_image_urls, image_path, amazon_image_name, count
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
def _is_amazon_delivered(
|
||||
self, msg_data: list, subjects: list[str]
|
||||
) -> tuple[bool, list[str]]:
|
||||
"""Verify if email is a delivered notification and return image URLs."""
|
||||
for response_part in msg_data:
|
||||
if not isinstance(response_part, (bytes, bytearray)):
|
||||
continue
|
||||
msg = email.message_from_bytes(response_part)
|
||||
subject = get_decoded_subject(msg)
|
||||
if not subject:
|
||||
continue
|
||||
|
||||
# Check if subject contains any delivered keyword (case-insensitive)
|
||||
has_delivered = any(s.lower() in subject.lower() for s in subjects)
|
||||
# Check if subject contains ordered or shipped keywords (case-insensitive)
|
||||
has_ordered = any(
|
||||
s.lower() in subject.lower() for s in AMAZON_ORDERED_SUBJECT
|
||||
)
|
||||
has_shipped = any(
|
||||
s.lower() in subject.lower() for s in AMAZON_SHIPMENT_SUBJECT
|
||||
)
|
||||
|
||||
if has_delivered and not has_ordered and not has_shipped:
|
||||
urls = self._extract_amazon_image_urls(msg)
|
||||
return True, urls
|
||||
return False, []
|
||||
|
||||
def _extract_amazon_image_urls(self, msg: email.message.Message) -> list[str]:
|
||||
"""Extract image URLs from Amazon email body."""
|
||||
urls = []
|
||||
pattern = re.compile(rf"{const.AMAZON_IMG_PATTERN}")
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() != "text/html":
|
||||
continue
|
||||
part_payload = part.get_payload(decode=True)
|
||||
if part_payload:
|
||||
part_content = part_payload.decode("utf-8", "ignore")
|
||||
found = pattern.findall(part_content)
|
||||
for url in found:
|
||||
if url[1] not in const.AMAZON_IMG_LIST:
|
||||
continue
|
||||
full_url = url[0] + url[1] + url[2]
|
||||
if full_url not in urls:
|
||||
urls.append(full_url)
|
||||
return urls
|
||||
|
||||
async def _process_amazon_images(
|
||||
self,
|
||||
image_urls: list[str],
|
||||
image_base_path: str,
|
||||
image_name: str,
|
||||
email_count: int,
|
||||
) -> None:
|
||||
"""Process and save Amazon delivery images."""
|
||||
if not image_base_path or not image_name:
|
||||
return
|
||||
|
||||
amazon_path = Path(image_base_path) / "amazon"
|
||||
image_files = await self._download_all_images(image_urls, image_base_path)
|
||||
|
||||
if len(image_files) > 1:
|
||||
await self._create_amazon_gif(image_files, amazon_path, image_name)
|
||||
elif len(image_files) == 1:
|
||||
await self._save_single_amazon_image(
|
||||
image_files[0], amazon_path, image_name
|
||||
)
|
||||
else:
|
||||
await self._copy_amazon_placeholder(amazon_path, image_name)
|
||||
|
||||
async def _download_all_images(self, urls: list[str], base_path: str) -> list[str]:
|
||||
"""Download all image URLs to temporary files."""
|
||||
image_files = []
|
||||
amazon_path = Path(base_path) / "amazon"
|
||||
for url in urls:
|
||||
temp_filename = random_filename()
|
||||
await download_amazon_img(url, base_path, temp_filename, self.hass)
|
||||
full_temp_path = amazon_path / temp_filename
|
||||
if await anyio.Path(full_temp_path).exists():
|
||||
image_files.append(str(full_temp_path))
|
||||
return image_files
|
||||
|
||||
async def _create_amazon_gif(
|
||||
self, image_files: list[str], amazon_path: Path, image_name: str
|
||||
) -> None:
|
||||
"""Create animated GIF from multiple images."""
|
||||
_LOGGER.debug("Combining %d Amazon images into GIF", len(image_files))
|
||||
resized_images = await self.hass.async_add_executor_job(
|
||||
resize_images, image_files, 724, 320
|
||||
)
|
||||
gif_path = str(amazon_path / image_name)
|
||||
duration = self.config.get(CONF_DURATION, 5) * 1000
|
||||
await self.hass.async_add_executor_job(
|
||||
generate_delivery_gif, resized_images, gif_path, duration
|
||||
)
|
||||
# Cleanup
|
||||
for img in image_files + resized_images:
|
||||
if await anyio.Path(img).exists():
|
||||
await self.hass.async_add_executor_job(
|
||||
cleanup_images, str(Path(img).parent) + "/", Path(img).name
|
||||
)
|
||||
|
||||
async def _save_single_amazon_image(
|
||||
self, image_file: str, amazon_path: Path, image_name: str
|
||||
) -> None:
|
||||
"""Save a single image by renaming it to the final name."""
|
||||
final_path = amazon_path / image_name
|
||||
if await anyio.Path(final_path).exists():
|
||||
await anyio.Path(final_path).unlink()
|
||||
await self.hass.async_add_executor_job(Path(image_file).rename, final_path)
|
||||
_LOGGER.debug("Single Amazon image saved: %s", image_name)
|
||||
|
||||
async def _copy_amazon_placeholder(
|
||||
self, amazon_path: Path, image_name: str
|
||||
) -> None:
|
||||
"""Copy the Amazon no-delivery placeholder."""
|
||||
nomail = f"{Path(__file__).parent.parent}/no_deliveries_amazon.jpg"
|
||||
_LOGGER.debug("No Amazon images found in emails, using placeholder")
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
copyfile, nomail, str(amazon_path / image_name)
|
||||
)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error attempting to copy image: %s", err)
|
||||
|
||||
async def _amazon_hub(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
fwds: list[str] | None = None,
|
||||
cache: EmailCache | None = None,
|
||||
forwarding_header: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Find Amazon Hub code."""
|
||||
_LOGGER.debug("=== AMAZON HUB SEARCH START ===")
|
||||
count = 0
|
||||
code = []
|
||||
processed_ids = []
|
||||
today = get_today().strftime("%d-%b-%Y")
|
||||
address_list = amazon_email_addresses(fwds, "amazon.com")
|
||||
for search_subject in AMAZON_HUB_SUBJECT:
|
||||
(server_response, data) = await email_search(
|
||||
account,
|
||||
address_list,
|
||||
today,
|
||||
search_subject,
|
||||
body=AMAZON_HUB_BODY,
|
||||
header=forwarding_header,
|
||||
)
|
||||
if server_response == "OK" and data[0] is not None:
|
||||
for num in data[0].split():
|
||||
if num in processed_ids:
|
||||
continue
|
||||
processed_ids.append(num)
|
||||
if cache:
|
||||
msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
|
||||
else:
|
||||
msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
|
||||
for response_part in msg_parts:
|
||||
if isinstance(response_part, (bytes, bytearray)):
|
||||
msg = email.message_from_bytes(response_part)
|
||||
actual_subject = get_decoded_subject(msg)
|
||||
body = get_email_body(msg)
|
||||
if hub_code := _extract_hub_code(
|
||||
body,
|
||||
AMAZON_HUB_BODY,
|
||||
actual_subject,
|
||||
AMAZON_HUB_SUBJECT_SEARCH,
|
||||
):
|
||||
count += 1
|
||||
if hub_code not in code:
|
||||
code.append(hub_code)
|
||||
return {AMAZON_HUB: count, AMAZON_HUB_CODE: code}
|
||||
|
||||
async def _amazon_otp(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
fwds: list[str] | None = None,
|
||||
cache: EmailCache | None = None,
|
||||
forwarding_header: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Find Amazon OTP code."""
|
||||
code = []
|
||||
today = get_today().strftime("%d-%b-%Y")
|
||||
address_list = amazon_email_addresses(fwds, "amazon.com")
|
||||
(server_response, data) = await email_search(
|
||||
account,
|
||||
address_list,
|
||||
today,
|
||||
AMAZON_OTP_SUBJECT,
|
||||
body=AMAZON_OTP_REGEX,
|
||||
header=forwarding_header,
|
||||
)
|
||||
if server_response == "OK" and data[0] is not None:
|
||||
for num in data[0].split():
|
||||
if cache:
|
||||
msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
|
||||
else:
|
||||
msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
|
||||
for response_part in msg_parts:
|
||||
if isinstance(response_part, (bytes, bytearray)):
|
||||
msg = email.message_from_bytes(response_part)
|
||||
body = get_email_body(msg)
|
||||
if (
|
||||
found := re.compile(AMAZON_OTP_REGEX).search(body)
|
||||
) is not None:
|
||||
code.append(found.group(2))
|
||||
return {AMAZON_OTP: len(code), AMAZON_OTP_CODE: code}
|
||||
|
||||
async def _amazon_exception(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
fwds: list[str] | None = None,
|
||||
domain: str | None = None,
|
||||
cache: EmailCache | None = None,
|
||||
forwarding_header: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Find Amazon exception emails."""
|
||||
count = 0
|
||||
orders = []
|
||||
today = get_today().strftime("%d-%b-%Y")
|
||||
address_list = amazon_email_addresses(fwds, domain)
|
||||
(server_response, data) = await email_search(
|
||||
account=account,
|
||||
address=address_list,
|
||||
date=today,
|
||||
subject=AMAZON_EXCEPTION_SUBJECT,
|
||||
header=forwarding_header,
|
||||
)
|
||||
if server_response == "OK" and data[0] is not None:
|
||||
order_pattern = re.compile(r"[0-9]{3}-[0-9]{7}-[0-9]{7}")
|
||||
for num in data[0].split():
|
||||
if cache:
|
||||
msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
|
||||
else:
|
||||
msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
|
||||
for response_part in msg_parts:
|
||||
if isinstance(response_part, (bytes, bytearray)):
|
||||
msg = email.message_from_bytes(response_part)
|
||||
body = get_email_body(msg)
|
||||
subject = get_decoded_subject(msg)
|
||||
if AMAZON_EXCEPTION_BODY in body:
|
||||
count += 1
|
||||
if found := order_pattern.findall(body):
|
||||
orders.extend(found)
|
||||
if found := order_pattern.findall(subject):
|
||||
orders.extend(found)
|
||||
return {AMAZON_EXCEPTION: count, AMAZON_EXCEPTION_ORDER: orders}
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Base Shipper class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from aioimaplib import IMAP4_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
|
||||
|
||||
class Shipper(ABC):
|
||||
"""Base class for shipper-specific parsing logic."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Initialize the shipper."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Return the internal name of the shipper."""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def handles_sensor(cls, sensor_type: str) -> bool:
|
||||
"""Return True if this shipper handles the given sensor type."""
|
||||
|
||||
@abstractmethod
|
||||
async def process(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensor_type: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Process emails for this shipper on the given date for a specific sensor."""
|
||||
|
||||
@abstractmethod
|
||||
async def process_batch(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensors: list[str],
|
||||
cache: EmailCache,
|
||||
since_date: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process multiple sensors for this shipper using batched fetching/searching.
|
||||
|
||||
since_date: earliest IMAP SINCE date for _delivering/_exception/_delivered
|
||||
sensors. Defaults to date (today) if not provided.
|
||||
"""
|
||||
@@ -0,0 +1,697 @@
|
||||
"""Generic Shipper class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import email
|
||||
import logging
|
||||
from email.header import decode_header
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
from aioimaplib import IMAP4_SSL
|
||||
|
||||
from custom_components.mail_and_packages.const import (
|
||||
AMAZON_DELIEVERED_BY_OTHERS_SEARCH_TEXT,
|
||||
AMAZON_DELIVERED,
|
||||
ATTR_BODY,
|
||||
ATTR_BODY_COUNT,
|
||||
ATTR_COUNT,
|
||||
ATTR_EMAIL,
|
||||
ATTR_PATTERN,
|
||||
ATTR_SUBJECT,
|
||||
ATTR_TRACKING,
|
||||
CAMERA_DATA,
|
||||
CAMERA_EXTRACTION_CONFIG,
|
||||
CONF_FORWARDING_HEADER,
|
||||
SENSOR_DATA,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
from custom_components.mail_and_packages.utils.email import find_text, find_text_matches
|
||||
from custom_components.mail_and_packages.utils.imap import (
|
||||
email_fetch,
|
||||
email_fetch_headers,
|
||||
email_search,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.shipper import (
|
||||
generic_delivery_image_extraction,
|
||||
get_tracking,
|
||||
)
|
||||
|
||||
from .base import Shipper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GenericShipper(Shipper):
|
||||
"""Generic Shipper class for UPS, FedEx, Walmart, etc."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the internal name of the shipper."""
|
||||
return "generic"
|
||||
|
||||
@classmethod
|
||||
def handles_sensor(cls, sensor_type: str) -> bool:
|
||||
"""Return True if this shipper handles the given sensor type."""
|
||||
return sensor_type in SENSOR_DATA
|
||||
|
||||
async def process(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensor_type: str,
|
||||
cache: EmailCache | None = None,
|
||||
since_date: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process emails for this shipper on the given date.
|
||||
|
||||
since_date: if provided, used instead of date for _delivering and
|
||||
_exception sensors so that emails from previous days are included.
|
||||
"""
|
||||
_LOGGER.debug("Processing generic sensor: %s", sensor_type)
|
||||
|
||||
if sensor_type not in SENSOR_DATA:
|
||||
_LOGGER.error("Sensor %s not found in SENSOR_DATA", sensor_type)
|
||||
return {ATTR_COUNT: 0}
|
||||
|
||||
config = SENSOR_DATA[sensor_type]
|
||||
email_addresses = config.get(ATTR_EMAIL, [])
|
||||
subjects = config.get(ATTR_SUBJECT, [])
|
||||
|
||||
# _packages sensors with no email/subject are computed in process_batch
|
||||
# as delivering + delivered; skip IMAP search here.
|
||||
if sensor_type.endswith("_packages") and not email_addresses and not subjects:
|
||||
_LOGGER.debug(
|
||||
"Skipping email search for %s: no email addresses configured",
|
||||
sensor_type,
|
||||
)
|
||||
return {ATTR_COUNT: 0, ATTR_TRACKING: []}
|
||||
|
||||
forwarding_header, email_addresses = self._resolve_forwarding(email_addresses)
|
||||
|
||||
# _delivering/_exception/_packages use the extended window so in-transit
|
||||
# packages remain visible across the midnight boundary.
|
||||
# _delivered uses today's date for the sensor count (resets at midnight)
|
||||
# but also searches the extended window to obtain tracking numbers for
|
||||
# deduplication — without those, a package delivered yesterday would still
|
||||
# appear as "delivering" today because the delivering email is in the window
|
||||
# but the delivered email is not.
|
||||
is_delivered = sensor_type.endswith("_delivered")
|
||||
search_date = date
|
||||
if (
|
||||
since_date
|
||||
and sensor_type.endswith(
|
||||
("_delivering", "_exception", "_delivered", "_packages")
|
||||
)
|
||||
and sensor_type != "post_de_delivering"
|
||||
):
|
||||
search_date = since_date
|
||||
|
||||
result = {ATTR_COUNT: 0, ATTR_TRACKING: []}
|
||||
|
||||
# Skip email search for sensors with no email addresses configured
|
||||
# (e.g. *_packages sensors that are empty dicts in SENSOR_DATA)
|
||||
if not email_addresses:
|
||||
_LOGGER.debug(
|
||||
"Skipping email search for %s: no email addresses configured",
|
||||
sensor_type,
|
||||
)
|
||||
return result
|
||||
|
||||
image_path = self.config.get("image_path")
|
||||
# Setup image extraction
|
||||
shipper_cfg = await self._setup_image_extraction(sensor_type, image_path)
|
||||
image_found = False
|
||||
|
||||
count, found_data, image_found = await self._search_for_emails(
|
||||
account,
|
||||
email_addresses,
|
||||
search_date,
|
||||
subjects,
|
||||
config,
|
||||
shipper_cfg,
|
||||
sensor_type,
|
||||
result,
|
||||
cache,
|
||||
forwarding_header,
|
||||
)
|
||||
|
||||
# Process tracking numbers
|
||||
result[ATTR_TRACKING] = await self._process_tracking_numbers(
|
||||
sensor_type,
|
||||
found_data,
|
||||
account,
|
||||
cache,
|
||||
)
|
||||
if result[ATTR_TRACKING]:
|
||||
count = len(result[ATTR_TRACKING])
|
||||
|
||||
if is_delivered:
|
||||
result["pre_filtered_tracking"] = result.get(ATTR_TRACKING, [])
|
||||
|
||||
# For _delivered sensors, the extended-window search gives us tracking
|
||||
# numbers needed for deduplication (above), but the count must reflect
|
||||
# only today's deliveries so the sensor resets at midnight.
|
||||
if is_delivered and since_date and search_date != date:
|
||||
today_result: dict[str, Any] = {ATTR_COUNT: 0, ATTR_TRACKING: []}
|
||||
today_count, today_found, _ = await self._search_for_emails(
|
||||
account,
|
||||
email_addresses,
|
||||
date,
|
||||
subjects,
|
||||
config,
|
||||
shipper_cfg,
|
||||
sensor_type,
|
||||
today_result,
|
||||
cache,
|
||||
forwarding_header,
|
||||
)
|
||||
today_tracking = await self._process_tracking_numbers(
|
||||
sensor_type, today_found, account, cache
|
||||
)
|
||||
count = len(today_tracking) if today_tracking else today_count
|
||||
result[ATTR_TRACKING] = today_tracking
|
||||
|
||||
result[ATTR_COUNT] = count
|
||||
if shipper_cfg:
|
||||
image_attr = f"{shipper_cfg['name']}_image"
|
||||
result[image_attr] = shipper_cfg["image_name"]
|
||||
result["image_path"] = image_path
|
||||
|
||||
if not image_found:
|
||||
await self._copy_generic_placeholder(shipper_cfg)
|
||||
|
||||
return result
|
||||
|
||||
def _resolve_forwarding(self, email_addresses: list[str]) -> tuple[str, list[str]]:
|
||||
"""Return (forwarding_header, resolved_email_addresses).
|
||||
|
||||
Header mode: uses original-sender header for matching; address list
|
||||
is passed as-is so IMAP can match via HEADER substring.
|
||||
Address-list mode: prepends the user's forwarded addresses so that
|
||||
emails arriving through a forwarding service are also matched.
|
||||
"""
|
||||
forwarding_header = self.config.get(CONF_FORWARDING_HEADER, "")
|
||||
if forwarding_header and forwarding_header != "(none)":
|
||||
return forwarding_header, email_addresses
|
||||
forwarding_header = ""
|
||||
forwarded_emails = self.config.get("forwarded_emails", [])
|
||||
if isinstance(forwarded_emails, str):
|
||||
forwarded_emails = [
|
||||
e.strip() for e in forwarded_emails.split(",") if e.strip()
|
||||
]
|
||||
if forwarded_emails:
|
||||
email_addresses = forwarded_emails + email_addresses
|
||||
return forwarding_header, email_addresses
|
||||
|
||||
async def process_batch(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensors: list[str],
|
||||
cache: EmailCache,
|
||||
since_date: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process multiple generic sensors in batch."""
|
||||
batch_results, all_tracking = await self._process_individual_sensors(
|
||||
account, date, sensors, cache, since_date
|
||||
)
|
||||
|
||||
self._deduplicate_batch_tracking(batch_results)
|
||||
self._compute_package_totals(batch_results)
|
||||
|
||||
# Merge results and aggregate global tracking
|
||||
res = {}
|
||||
for sensor, sensor_res in batch_results:
|
||||
tracking = (
|
||||
sensor_res.pop("pre_filtered_tracking", [])
|
||||
if sensor.endswith("_delivered")
|
||||
else sensor_res.get(ATTR_TRACKING)
|
||||
)
|
||||
res.update(sensor_res)
|
||||
# Expose per-sensor raw tracking for coordinator state management.
|
||||
# Keyed as "_tracking_details" to distinguish from the public data dict.
|
||||
if tracking and sensor.endswith(
|
||||
("_delivering", "_delivered", "_exception")
|
||||
):
|
||||
res.setdefault("_tracking_details", {})[sensor] = list(tracking)
|
||||
|
||||
if all_tracking:
|
||||
res[ATTR_TRACKING] = list(all_tracking)
|
||||
|
||||
return res
|
||||
|
||||
async def _process_individual_sensors(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensors: list[str],
|
||||
cache: EmailCache,
|
||||
since_date: str | None = None,
|
||||
) -> tuple[list[tuple[str, dict[str, Any]]], set[str]]:
|
||||
"""Process each sensor independently and aggregate tracking."""
|
||||
batch_results = []
|
||||
all_tracking = set()
|
||||
|
||||
for sensor in sensors:
|
||||
sensor_res = await self.process(
|
||||
account, date, sensor, cache, since_date=since_date
|
||||
)
|
||||
# Replicate coordinator dictionary logic for local sensor counts
|
||||
if sensor not in sensor_res and ATTR_COUNT in sensor_res:
|
||||
sensor_res[sensor] = sensor_res[ATTR_COUNT]
|
||||
|
||||
# Capture today-only tracking for _delivered sensors BEFORE
|
||||
# _deduplicate_batch_tracking runs (which currently only modifies
|
||||
# _delivering and _packages sensor results).
|
||||
if sensor_res.get(ATTR_TRACKING) and sensor.endswith("_delivered"):
|
||||
sensor_res[f"{sensor}_tracking"] = sensor_res[ATTR_TRACKING]
|
||||
|
||||
# Record results for post-processing
|
||||
batch_results.append((sensor, sensor_res))
|
||||
|
||||
# Aggregate all tracking numbers found
|
||||
if sensor_res.get(ATTR_TRACKING):
|
||||
all_tracking.update(sensor_res[ATTR_TRACKING])
|
||||
|
||||
return batch_results, all_tracking
|
||||
|
||||
def _deduplicate_batch_tracking(
|
||||
self,
|
||||
batch_results: list[tuple[str, dict[str, Any]]],
|
||||
) -> None:
|
||||
"""Deduplicate tracking numbers across sensors based on shipper prefix."""
|
||||
shippers = {}
|
||||
for sensor, sensor_res in batch_results:
|
||||
# Prefix is everything before the last underscore (e.g., 'ups', 'fedex')
|
||||
prefix = "_".join(sensor.split("_")[:-1])
|
||||
if prefix not in shippers:
|
||||
shippers[prefix] = {
|
||||
"delivered": set(),
|
||||
"delivering": set(),
|
||||
"update_targets": [],
|
||||
"package_targets": [],
|
||||
}
|
||||
|
||||
tracking = set(sensor_res.get(ATTR_TRACKING, []))
|
||||
if sensor.endswith("_delivered"):
|
||||
shippers[prefix]["delivered"].update(tracking)
|
||||
elif sensor.endswith(("_delivering", "_exception")):
|
||||
shippers[prefix]["delivering"].update(tracking)
|
||||
shippers[prefix]["update_targets"].append((sensor, sensor_res))
|
||||
elif sensor.endswith("_packages"):
|
||||
shippers[prefix]["package_targets"].append((sensor, sensor_res))
|
||||
|
||||
for data in shippers.values():
|
||||
# Remove "delivered" tracking numbers from in-transit sensors
|
||||
self._apply_deduplication(data["update_targets"], data["delivered"])
|
||||
# Remove "delivering" and "delivered" tracking numbers from _packages
|
||||
# so _packages only shows packages not yet out for delivery or delivered
|
||||
in_pipeline = data["delivering"] | data["delivered"]
|
||||
self._apply_deduplication(data["package_targets"], in_pipeline)
|
||||
|
||||
def _apply_deduplication(
|
||||
self,
|
||||
targets: list[tuple[str, dict[str, Any]]],
|
||||
delivered_ids: set[str],
|
||||
) -> None:
|
||||
"""Apply deduplication logic to a list of target sensors."""
|
||||
if not delivered_ids:
|
||||
return
|
||||
|
||||
for sensor, sensor_res in targets:
|
||||
original_tracking = sensor_res.get(ATTR_TRACKING, [])
|
||||
new_tracking = [
|
||||
tid for tid in original_tracking if tid not in delivered_ids
|
||||
]
|
||||
|
||||
if len(new_tracking) != len(original_tracking):
|
||||
sensor_res[ATTR_TRACKING] = new_tracking
|
||||
sensor_res[sensor] = len(new_tracking)
|
||||
if ATTR_COUNT in sensor_res:
|
||||
sensor_res[ATTR_COUNT] = len(new_tracking)
|
||||
|
||||
def _compute_package_totals(
|
||||
self,
|
||||
batch_results: list[tuple[str, dict[str, Any]]],
|
||||
) -> None:
|
||||
"""Compute _packages sensors with empty config as delivering + delivered.
|
||||
|
||||
These sensors have no IMAP search of their own; their value is the
|
||||
sum of the shipper's _delivering and _delivered counts (matching the
|
||||
original pre-refactor behaviour in helpers.py).
|
||||
"""
|
||||
sensor_counts = {
|
||||
sensor: sensor_res.get(sensor, sensor_res.get(ATTR_COUNT, 0))
|
||||
for sensor, sensor_res in batch_results
|
||||
}
|
||||
|
||||
for sensor, sensor_res in batch_results:
|
||||
if not sensor.endswith("_packages"):
|
||||
continue
|
||||
config = SENSOR_DATA.get(sensor, {})
|
||||
if config.get(ATTR_EMAIL) or config.get(ATTR_SUBJECT):
|
||||
continue # sensor has its own IMAP search config
|
||||
prefix = sensor.replace("_packages", "")
|
||||
computed = sensor_counts.get(f"{prefix}_delivering", 0) + sensor_counts.get(
|
||||
f"{prefix}_delivered", 0
|
||||
)
|
||||
sensor_res[sensor] = computed
|
||||
sensor_res[ATTR_COUNT] = computed
|
||||
|
||||
async def _copy_generic_placeholder(self, shipper_cfg: dict[str, Any]) -> None:
|
||||
"""Copy the generic placeholder for the shipper."""
|
||||
shipper_name = shipper_cfg["name"]
|
||||
# Try to find courier-specific placeholder
|
||||
placeholder = Path(__file__).parent.parent / f"no_deliveries_{shipper_name}.jpg"
|
||||
if not await anyio.Path(placeholder).exists():
|
||||
placeholder = Path(__file__).parent.parent / "mail_none.gif"
|
||||
|
||||
target = (
|
||||
Path(shipper_cfg["image_path"]) / shipper_name / shipper_cfg["image_name"]
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"No %s images found in emails, using placeholder: %s",
|
||||
shipper_name,
|
||||
placeholder.name,
|
||||
)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
copyfile, str(placeholder), str(target)
|
||||
)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error attempting to copy placeholder: %s", err)
|
||||
|
||||
async def _search_for_emails(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
email_addresses: list[str],
|
||||
date: str,
|
||||
subjects: list[str],
|
||||
config: dict[str, Any],
|
||||
shipper_cfg: dict[str, Any] | None,
|
||||
sensor_type: str,
|
||||
result: dict[str, Any],
|
||||
cache: EmailCache | None = None,
|
||||
forwarding_header: str = "",
|
||||
) -> tuple[int, list[bytes], bool]:
|
||||
"""Search for and process emails."""
|
||||
count = 0
|
||||
unique_email_ids = set()
|
||||
found_data = []
|
||||
image_found = False
|
||||
|
||||
(server_response, sdata) = await email_search(
|
||||
account=account,
|
||||
address=email_addresses,
|
||||
date=date,
|
||||
subject=subjects,
|
||||
body=config.get(ATTR_BODY, ""),
|
||||
header=forwarding_header,
|
||||
)
|
||||
|
||||
if server_response == "OK" and sdata[0]:
|
||||
raw_ids = sdata[0].split()
|
||||
_LOGGER.debug(
|
||||
"Found %d matching email IDs for %s: %s",
|
||||
len(raw_ids),
|
||||
sensor_type,
|
||||
[eid.decode() if isinstance(eid, bytes) else eid for eid in raw_ids],
|
||||
)
|
||||
verified_ids = await self._verify_matched_subjects(
|
||||
account, raw_ids, sensor_type, subjects, cache
|
||||
)
|
||||
filtered_new_ids = self._filter_unique_ids(verified_ids, unique_email_ids)
|
||||
|
||||
if filtered_new_ids:
|
||||
count, img_found = await self._process_matched_emails(
|
||||
account,
|
||||
config,
|
||||
filtered_new_ids,
|
||||
count,
|
||||
cache,
|
||||
shipper_cfg,
|
||||
sensor_type,
|
||||
result,
|
||||
found_data,
|
||||
)
|
||||
if img_found:
|
||||
image_found = True
|
||||
|
||||
return count, found_data, image_found
|
||||
|
||||
def _decode_subject(self, header_part: bytes | bytearray) -> str | None:
|
||||
"""Decode MIME encoded subject from email header part."""
|
||||
msg = email.message_from_bytes(header_part)
|
||||
header_val = msg.get("subject")
|
||||
if not header_val:
|
||||
return None
|
||||
|
||||
decoded = decode_header(header_val)[0]
|
||||
subject_bytes, encoding = decoded
|
||||
if encoding:
|
||||
try:
|
||||
if isinstance(subject_bytes, bytes):
|
||||
return subject_bytes.decode(encoding, "ignore").strip()
|
||||
return str(subject_bytes).strip()
|
||||
except (LookupError, UnicodeError):
|
||||
pass
|
||||
|
||||
if isinstance(subject_bytes, bytes):
|
||||
return subject_bytes.decode("utf-8", "ignore").strip()
|
||||
return str(subject_bytes).strip()
|
||||
|
||||
async def _verify_matched_subjects(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
email_ids: list[bytes],
|
||||
sensor_type: str,
|
||||
expected_subjects: list[str],
|
||||
cache: EmailCache | None = None,
|
||||
) -> list[bytes]:
|
||||
"""Verify the subject of each matched email locally and log for debugging."""
|
||||
if not expected_subjects:
|
||||
return email_ids
|
||||
|
||||
verified_ids = []
|
||||
expected_subjects_lower = [s.lower() for s in expected_subjects]
|
||||
|
||||
for eid in email_ids:
|
||||
try:
|
||||
if cache:
|
||||
header_data = (
|
||||
await cache.fetch(eid, "(BODY[HEADER.FIELDS (SUBJECT)])")
|
||||
)[1]
|
||||
else:
|
||||
header_data = (await email_fetch_headers(account, eid))[1]
|
||||
|
||||
subject_found = False
|
||||
for part in header_data:
|
||||
if isinstance(part, (bytes, bytearray)):
|
||||
subject = self._decode_subject(part)
|
||||
if not subject:
|
||||
continue
|
||||
|
||||
_LOGGER.debug(
|
||||
"Matched email for %s (ID %s): %s",
|
||||
sensor_type,
|
||||
eid.decode() if isinstance(eid, bytes) else eid,
|
||||
subject,
|
||||
)
|
||||
subject_lower = subject.lower()
|
||||
if any(
|
||||
expected in subject_lower
|
||||
for expected in expected_subjects_lower
|
||||
):
|
||||
subject_found = True
|
||||
|
||||
if subject_found:
|
||||
verified_ids.append(eid)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Email ID %s rejected for %s: Subject did not match any expected subjects.",
|
||||
eid.decode() if isinstance(eid, bytes) else eid,
|
||||
sensor_type,
|
||||
)
|
||||
except (OSError, AttributeError) as err:
|
||||
_LOGGER.debug("Could not fetch subject for email %s: %s", eid, err)
|
||||
|
||||
return verified_ids
|
||||
|
||||
def _filter_unique_ids(
|
||||
self, email_ids: list[bytes], unique_email_ids: set
|
||||
) -> list[bytes]:
|
||||
"""Filter out already processed email IDs."""
|
||||
new_ids = []
|
||||
for eid in email_ids:
|
||||
eid_str = eid.decode() if isinstance(eid, bytes) else str(eid)
|
||||
if eid_str not in unique_email_ids:
|
||||
unique_email_ids.add(eid_str)
|
||||
new_ids.append(eid)
|
||||
return new_ids
|
||||
|
||||
async def _process_matched_emails(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
config: dict[str, Any],
|
||||
new_ids: list[bytes],
|
||||
current_count: int,
|
||||
cache: EmailCache | None,
|
||||
shipper_cfg: dict[str, Any] | None,
|
||||
sensor_type: str,
|
||||
result: dict[str, Any],
|
||||
found_data: list[bytes],
|
||||
) -> tuple[int, bool]:
|
||||
"""Process a batch of matched unique emails."""
|
||||
image_found = False
|
||||
count, matched_ids = await self._process_emails_by_type(
|
||||
account, config, new_ids, current_count, cache
|
||||
)
|
||||
if matched_ids:
|
||||
found_data.append(b" ".join(matched_ids))
|
||||
|
||||
if shipper_cfg:
|
||||
if await self._extract_images_for_shipper(
|
||||
account, matched_ids, shipper_cfg, cache
|
||||
):
|
||||
image_found = True
|
||||
|
||||
if sensor_type.endswith("_delivered") and sensor_type != AMAZON_DELIVERED:
|
||||
await self._check_amazon_mentions(account, matched_ids, result, cache)
|
||||
|
||||
return count, image_found
|
||||
|
||||
async def _process_tracking_numbers(
|
||||
self,
|
||||
sensor_type: str,
|
||||
found_data: list,
|
||||
account: IMAP4_SSL,
|
||||
cache: EmailCache | None = None,
|
||||
) -> list:
|
||||
"""Process tracking numbers for the sensor."""
|
||||
tracking_key = f"{'_'.join(sensor_type.split('_')[:-1])}_tracking"
|
||||
if (
|
||||
tracking_key not in SENSOR_DATA
|
||||
or ATTR_PATTERN not in SENSOR_DATA[tracking_key]
|
||||
):
|
||||
return []
|
||||
|
||||
pattern = SENSOR_DATA[tracking_key][ATTR_PATTERN][0]
|
||||
tracking_nums = []
|
||||
for sdata in found_data:
|
||||
tracking_nums.extend(
|
||||
await get_tracking(sdata.decode(), account, pattern, cache)
|
||||
)
|
||||
|
||||
return list(dict.fromkeys(tracking_nums))
|
||||
|
||||
async def _setup_image_extraction(
|
||||
self,
|
||||
sensor_type: str,
|
||||
image_path: str,
|
||||
) -> dict | None:
|
||||
"""Set up image extraction configuration."""
|
||||
if not sensor_type.endswith("_delivered"):
|
||||
return None
|
||||
|
||||
shipper_name = sensor_type.replace("_delivered", "")
|
||||
camera_key = f"{shipper_name}_camera"
|
||||
if camera_key not in CAMERA_DATA or camera_key in (
|
||||
"usps_camera",
|
||||
"generic_camera",
|
||||
):
|
||||
return None
|
||||
|
||||
extraction_config = CAMERA_EXTRACTION_CONFIG.get(shipper_name, {})
|
||||
absolute_image_path = image_path.rstrip("/") + "/"
|
||||
|
||||
def _create_dir():
|
||||
path = Path(absolute_image_path) / shipper_name
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
await self.hass.async_add_executor_job(_create_dir)
|
||||
|
||||
return {
|
||||
"name": shipper_name,
|
||||
"image_path": absolute_image_path,
|
||||
"image_name": self.config.get(f"{shipper_name}_image")
|
||||
or f"{shipper_name}_delivery.jpg",
|
||||
"image_type": extraction_config.get("image_type", "jpeg"),
|
||||
"cid_name": extraction_config.get("cid_name"),
|
||||
"pattern": extraction_config.get("attachment_filename_pattern"),
|
||||
}
|
||||
|
||||
async def _process_emails_by_type(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
config: dict,
|
||||
ids: list,
|
||||
current_count: int,
|
||||
cache: EmailCache | None = None,
|
||||
) -> tuple[int, list]:
|
||||
"""Process emails based on body search or just count."""
|
||||
if ATTR_BODY in config:
|
||||
body_count = config.get(ATTR_BODY_COUNT, False)
|
||||
mock_data = (b" ".join(ids),)
|
||||
count, matched_ids = await find_text_matches(
|
||||
mock_data,
|
||||
account,
|
||||
config[ATTR_BODY],
|
||||
body_count,
|
||||
cache,
|
||||
)
|
||||
return current_count + count, matched_ids
|
||||
return current_count + len(ids), list(ids)
|
||||
|
||||
async def _extract_images_for_shipper(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
ids: list,
|
||||
s_config: dict,
|
||||
cache: EmailCache | None = None,
|
||||
) -> bool:
|
||||
"""Extract delivery images from emails."""
|
||||
image_found = False
|
||||
for eid in ids:
|
||||
if cache:
|
||||
msg_parts = (await cache.fetch(eid, "(RFC822)"))[1]
|
||||
else:
|
||||
msg_parts = (await email_fetch(account, eid, "(RFC822)"))[1]
|
||||
for response_part in msg_parts:
|
||||
if isinstance(response_part, (bytes, bytearray)):
|
||||
if generic_delivery_image_extraction(
|
||||
response_part,
|
||||
s_config["image_path"],
|
||||
s_config["image_name"],
|
||||
s_config["name"],
|
||||
s_config["image_type"],
|
||||
s_config["cid_name"],
|
||||
s_config["pattern"],
|
||||
):
|
||||
_LOGGER.debug("Extracted image for %s", s_config["name"])
|
||||
image_found = True
|
||||
return image_found
|
||||
|
||||
async def _check_amazon_mentions(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
ids: list,
|
||||
result: dict,
|
||||
cache: EmailCache | None = None,
|
||||
):
|
||||
"""Check for Amazon mentions in emails."""
|
||||
mock_data = (b" ".join(ids),)
|
||||
amazon_mentions = await find_text(
|
||||
mock_data,
|
||||
account,
|
||||
AMAZON_DELIEVERED_BY_OTHERS_SEARCH_TEXT,
|
||||
False,
|
||||
cache,
|
||||
)
|
||||
if amazon_mentions > 0:
|
||||
result["amazon_delivered_by_others"] = (
|
||||
result.get("amazon_delivered_by_others", 0) + amazon_mentions
|
||||
)
|
||||
@@ -0,0 +1,370 @@
|
||||
"""Post DE Shipper class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import email
|
||||
import io
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
from aioimaplib import IMAP4_SSL
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from custom_components.mail_and_packages.const import (
|
||||
ATTR_IMAGE_PATH,
|
||||
CONF_CUSTOM_IMG_FILE,
|
||||
CONF_DURATION,
|
||||
CONF_FORWARDING_HEADER,
|
||||
CONF_GENERATE_GRID,
|
||||
CONF_GENERATE_MP4,
|
||||
CONF_POST_DE_CUSTOM_IMG_FILE,
|
||||
DEFAULT_CUSTOM_IMG_FILE,
|
||||
SENSOR_DATA,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
from custom_components.mail_and_packages.utils.date import get_formatted_date
|
||||
from custom_components.mail_and_packages.utils.image import (
|
||||
_generate_mp4,
|
||||
cleanup_images,
|
||||
generate_delivery_gif,
|
||||
generate_grid_img,
|
||||
random_filename,
|
||||
resize_images,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.imap import email_fetch, email_search
|
||||
|
||||
from .base import Shipper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PostDEShipper(Shipper):
|
||||
"""Post DE Briefankündigung shipper."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return shipper name."""
|
||||
return "post_de"
|
||||
|
||||
@classmethod
|
||||
def handles_sensor(cls, sensor_type: str) -> bool:
|
||||
"""Return True if this shipper handles the given sensor type."""
|
||||
return sensor_type == "post_de_mail"
|
||||
|
||||
async def process(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensor_type: str,
|
||||
cache: EmailCache | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process Post DE Briefankündigung emails."""
|
||||
if sensor_type != "post_de_mail":
|
||||
return {sensor_type: 0}
|
||||
|
||||
config = self._get_config()
|
||||
image_count = 0
|
||||
images = []
|
||||
images_delete = []
|
||||
|
||||
(server_response, data) = await self._search_emails(account)
|
||||
|
||||
# Bail out on error
|
||||
if server_response != "OK" or data[0] is None:
|
||||
return {sensor_type: image_count}
|
||||
|
||||
# Setup image directory
|
||||
post_de_dir = Path(config["image_output_path"]) / "post_de"
|
||||
if not await self._setup_image_directory(str(post_de_dir)):
|
||||
return {sensor_type: image_count}
|
||||
|
||||
_LOGGER.debug("Post DE Briefankündigung email found processing...")
|
||||
for num in data[0].split():
|
||||
(image_count, images) = await self._process_post_de_email(
|
||||
account,
|
||||
num,
|
||||
str(post_de_dir),
|
||||
image_count,
|
||||
images,
|
||||
cache,
|
||||
)
|
||||
|
||||
image_count = len(images)
|
||||
|
||||
if image_count > 0:
|
||||
await self._generate_mail_image(
|
||||
images,
|
||||
str(post_de_dir),
|
||||
config["image_name"],
|
||||
config["gif_duration"],
|
||||
images_delete,
|
||||
)
|
||||
elif image_count == 0:
|
||||
await self._copy_nomail_image(
|
||||
str(post_de_dir),
|
||||
config["image_name"],
|
||||
config["custom_img"],
|
||||
)
|
||||
|
||||
if config["gen_mp4"]:
|
||||
await self._generate_mp4_video(
|
||||
str(post_de_dir),
|
||||
config["image_name"],
|
||||
)
|
||||
if config["gen_grid"]:
|
||||
await self._generate_grid_image(
|
||||
str(post_de_dir),
|
||||
config["image_name"],
|
||||
image_count,
|
||||
)
|
||||
|
||||
return {
|
||||
sensor_type: image_count,
|
||||
"post_de_image": config["image_name"],
|
||||
ATTR_IMAGE_PATH: config["image_output_path"],
|
||||
"post_de_grid_image_name": config["image_name"].replace(
|
||||
".gif", "_grid.png"
|
||||
),
|
||||
}
|
||||
|
||||
async def process_batch(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensors: list[str],
|
||||
cache: EmailCache,
|
||||
since_date: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process multiple Post DE sensors in batch."""
|
||||
res = {}
|
||||
for sensor in sensors:
|
||||
res.update(await self.process(account, date, sensor, cache))
|
||||
|
||||
# Replicate coordinator dict structure
|
||||
if sensor not in res:
|
||||
res[sensor] = res.get(sensor, 0)
|
||||
|
||||
return res
|
||||
|
||||
async def _generate_mp4_video(self, path: str, name: str):
|
||||
"""Generate MP4 video from images."""
|
||||
await self.hass.async_add_executor_job(_generate_mp4, path + "/", name)
|
||||
|
||||
async def _generate_grid_image(self, path: str, name: str, count: int):
|
||||
"""Generate grid image from images."""
|
||||
await self.hass.async_add_executor_job(
|
||||
generate_grid_img, path + "/", name, count
|
||||
)
|
||||
|
||||
async def _generate_mail_image(
|
||||
self,
|
||||
images: list,
|
||||
path: str,
|
||||
name: str,
|
||||
duration: int,
|
||||
delete_list: list,
|
||||
):
|
||||
"""Generate animated GIF from mail images."""
|
||||
try:
|
||||
_LOGGER.debug("Resizing Post DE images to 724x320...")
|
||||
all_images = await self.hass.async_add_executor_job(
|
||||
resize_images,
|
||||
images,
|
||||
724,
|
||||
320,
|
||||
)
|
||||
delete_list.extend(all_images)
|
||||
|
||||
_LOGGER.debug("Generating animated GIF for Post DE")
|
||||
gif_path = str(Path(path) / name)
|
||||
await self.hass.async_add_executor_job(
|
||||
generate_delivery_gif,
|
||||
all_images,
|
||||
gif_path,
|
||||
duration * 1000,
|
||||
)
|
||||
_LOGGER.debug("Post DE mail image generated.")
|
||||
except (OSError, ValueError) as err:
|
||||
_LOGGER.error("Error attempting to generate Post DE image: %s", err)
|
||||
|
||||
for image in delete_list:
|
||||
await self.hass.async_add_executor_job(
|
||||
cleanup_images,
|
||||
f"{Path(image).parent}/",
|
||||
Path(image).name,
|
||||
)
|
||||
|
||||
async def _copy_nomail_image(self, path: str, name: str, custom_img: str | None):
|
||||
"""Copy the 'no mail' placeholder image."""
|
||||
|
||||
def _prepare():
|
||||
if not Path(path).exists():
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
target = Path(path) / name
|
||||
if target.is_file():
|
||||
cleanup_images(path + "/", name)
|
||||
src = custom_img or str(Path(__file__).parent.parent / "mail_none.gif")
|
||||
shutil.copyfile(src, str(target))
|
||||
|
||||
_LOGGER.debug("No Post DE mail found.")
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_prepare)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error attempting to copy Post DE image: %s", err)
|
||||
|
||||
def _get_config(self) -> dict:
|
||||
"""Get Post DE specific configuration."""
|
||||
|
||||
image_path = self.config.get("image_path")
|
||||
return {
|
||||
"image_output_path": image_path,
|
||||
"gif_duration": self.config.get(CONF_DURATION),
|
||||
"image_name": self.config.get("post_de_image") or "post_de_deliveries.gif",
|
||||
"gen_mp4": self.config.get(CONF_GENERATE_MP4),
|
||||
"custom_img": self.config.get(CONF_POST_DE_CUSTOM_IMG_FILE)
|
||||
or self.config.get(CONF_CUSTOM_IMG_FILE)
|
||||
or DEFAULT_CUSTOM_IMG_FILE,
|
||||
"gen_grid": self.config.get(CONF_GENERATE_GRID),
|
||||
}
|
||||
|
||||
async def _search_emails(self, account: IMAP4_SSL) -> tuple:
|
||||
"""Search for Post DE Briefankündigung emails."""
|
||||
_LOGGER.debug("Attempting to find Post DE Briefankündigung mail")
|
||||
_LOGGER.debug("Post DE search date: %s", get_formatted_date())
|
||||
|
||||
config = SENSOR_DATA["post_de_mail"]
|
||||
email_addresses = config.get("email", [])
|
||||
subjects = config.get("subject", [])
|
||||
|
||||
forwarding_header = self.config.get(CONF_FORWARDING_HEADER, "")
|
||||
if forwarding_header and forwarding_header != "(none)":
|
||||
pass
|
||||
else:
|
||||
forwarding_header = ""
|
||||
forwarded_emails = self.config.get("forwarded_emails", [])
|
||||
if isinstance(forwarded_emails, str):
|
||||
forwarded_emails = [
|
||||
e.strip() for e in forwarded_emails.split(",") if e.strip()
|
||||
]
|
||||
if forwarded_emails:
|
||||
email_addresses = forwarded_emails + email_addresses
|
||||
|
||||
return await email_search(
|
||||
account=account,
|
||||
address=email_addresses,
|
||||
date=get_formatted_date(),
|
||||
subject=subjects,
|
||||
header=forwarding_header,
|
||||
)
|
||||
|
||||
async def _setup_image_directory(self, path: str) -> bool:
|
||||
"""Ensure image directory exists and is prepared."""
|
||||
if not await anyio.Path(path).is_dir():
|
||||
try:
|
||||
await anyio.Path(path).mkdir(parents=True, exist_ok=True)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error creating directory: %s", err)
|
||||
return False
|
||||
|
||||
# Clean up
|
||||
await self.hass.async_add_executor_job(cleanup_images, path + "/")
|
||||
return True
|
||||
|
||||
async def _process_post_de_email( # noqa: C901
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
num: str,
|
||||
image_output_path: str,
|
||||
image_count: int,
|
||||
images: list,
|
||||
cache: EmailCache | None = None,
|
||||
) -> tuple[int, list]:
|
||||
"""Process a single Post DE email and extract envelope scans.
|
||||
|
||||
Expected email payload format is HTML containing inline <img> tags
|
||||
referencing base64-encoded PNG/JPEG images.
|
||||
"""
|
||||
if cache:
|
||||
msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
|
||||
else:
|
||||
msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
|
||||
_LOGGER.debug("Processing Post DE email number: %s", num)
|
||||
for response_part in msg_parts:
|
||||
if isinstance(response_part, (bytes, bytearray)):
|
||||
msg = email.message_from_bytes(response_part)
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() in ("image/png", "image/jpeg"):
|
||||
payload = part.get_payload(decode=True)
|
||||
if not payload:
|
||||
continue
|
||||
|
||||
# Check image dimensions to skip logos/icons
|
||||
def _check_and_save(
|
||||
img_bytes: bytes,
|
||||
out_path: str,
|
||||
content_type: str,
|
||||
) -> str | None:
|
||||
try:
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
if img.format is None:
|
||||
_LOGGER.debug(
|
||||
"Post DE image format is unidentified (None)"
|
||||
)
|
||||
return None
|
||||
|
||||
# Validate format against expected content type
|
||||
if content_type == "image/png" and img.format != "PNG":
|
||||
_LOGGER.debug(
|
||||
"Post DE image format mismatch: expected PNG, got %s",
|
||||
img.format,
|
||||
)
|
||||
return None
|
||||
if (
|
||||
content_type == "image/jpeg"
|
||||
and img.format != "JPEG"
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Post DE image format mismatch: expected JPEG, got %s",
|
||||
img.format,
|
||||
)
|
||||
return None
|
||||
|
||||
width, height = img.size
|
||||
if width > 150 and height > 100:
|
||||
ext = (
|
||||
".png"
|
||||
if content_type == "image/png"
|
||||
else ".jpg"
|
||||
)
|
||||
filename = random_filename(ext=ext)
|
||||
target = Path(out_path) / filename
|
||||
with target.open("wb") as f:
|
||||
f.write(img_bytes)
|
||||
return str(target)
|
||||
except UnidentifiedImageError as err:
|
||||
_LOGGER.warning(
|
||||
"Unidentified image found in Post DE email: %s", err
|
||||
)
|
||||
except (OSError, ValueError, TypeError) as err:
|
||||
_LOGGER.debug(
|
||||
"Error checking/saving Post DE image: %s", err
|
||||
)
|
||||
return None
|
||||
|
||||
saved_path = await self.hass.async_add_executor_job(
|
||||
_check_and_save,
|
||||
payload,
|
||||
image_output_path,
|
||||
part.get_content_type(),
|
||||
)
|
||||
if saved_path:
|
||||
images.append(saved_path)
|
||||
image_count += 1
|
||||
_LOGGER.debug(
|
||||
"Extracted Post DE mail image: %s", saved_path
|
||||
)
|
||||
|
||||
return image_count, images
|
||||
@@ -0,0 +1,425 @@
|
||||
"""USPS Shipper class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import email
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
from aioimaplib import IMAP4_SSL
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from custom_components.mail_and_packages.const import (
|
||||
ATTR_COUNT,
|
||||
ATTR_EMAIL,
|
||||
ATTR_GRID_IMAGE_NAME,
|
||||
ATTR_IMAGE_PATH,
|
||||
ATTR_SUBJECT,
|
||||
ATTR_USPS_IMAGE,
|
||||
ATTR_USPS_MAIL,
|
||||
CONF_CUSTOM_IMG_FILE,
|
||||
CONF_DURATION,
|
||||
CONF_FORWARDING_HEADER,
|
||||
CONF_GENERATE_GRID,
|
||||
CONF_GENERATE_MP4,
|
||||
DEFAULT_CUSTOM_IMG_FILE,
|
||||
SENSOR_DATA,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
from custom_components.mail_and_packages.utils.date import get_formatted_date
|
||||
from custom_components.mail_and_packages.utils.image import (
|
||||
_generate_mp4,
|
||||
cleanup_images,
|
||||
copy_overlays,
|
||||
generate_delivery_gif,
|
||||
generate_grid_img,
|
||||
io_save_file,
|
||||
random_filename,
|
||||
resize_images,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.imap import email_fetch, email_search
|
||||
|
||||
from .base import Shipper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class USPSShipper(Shipper):
|
||||
"""USPS Informed Delivery shipper."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return shipper name."""
|
||||
return "usps"
|
||||
|
||||
@classmethod
|
||||
def handles_sensor(cls, sensor_type: str) -> bool:
|
||||
"""Return True if this shipper handles the given sensor type."""
|
||||
return sensor_type == "usps_mail"
|
||||
|
||||
async def process(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensor_type: str,
|
||||
cache: EmailCache | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process USPS Informed Delivery emails."""
|
||||
config = self._get_usps_config()
|
||||
image_count = 0
|
||||
images = []
|
||||
images_delete = []
|
||||
|
||||
(server_response, data) = await self._search_informed_delivery(account)
|
||||
|
||||
# Bail out on error
|
||||
if server_response != "OK" or data[0] is None:
|
||||
return {ATTR_COUNT: image_count}
|
||||
|
||||
# Setup image directory and overlays
|
||||
if not await self._setup_image_directory(config["image_output_path"]):
|
||||
return {ATTR_COUNT: image_count}
|
||||
|
||||
all_msg_content = ""
|
||||
if server_response == "OK":
|
||||
_LOGGER.debug("Informed Delivery email found processing...")
|
||||
for num in data[0].split():
|
||||
(image_count, images, email_content) = await self._process_usps_email(
|
||||
account,
|
||||
num,
|
||||
config["image_output_path"],
|
||||
image_count,
|
||||
images,
|
||||
cache,
|
||||
)
|
||||
all_msg_content += email_content
|
||||
|
||||
# Process images
|
||||
images = await self._process_usps_images(all_msg_content, images)
|
||||
image_count = len(images)
|
||||
|
||||
if image_count > 0:
|
||||
await self._generate_mail_image(
|
||||
images,
|
||||
config["image_output_path"],
|
||||
config["image_name"],
|
||||
config["gif_duration"],
|
||||
images_delete,
|
||||
)
|
||||
elif image_count == 0:
|
||||
await self._copy_nomail_image(
|
||||
config["image_output_path"],
|
||||
config["image_name"],
|
||||
config["custom_img"],
|
||||
)
|
||||
|
||||
if config["gen_mp4"]:
|
||||
await self._generate_mp4_video(
|
||||
config["image_output_path"],
|
||||
config["image_name"],
|
||||
)
|
||||
if config["gen_grid"]:
|
||||
await self._generate_grid_image(
|
||||
config["image_output_path"],
|
||||
config["image_name"],
|
||||
image_count,
|
||||
)
|
||||
|
||||
return {
|
||||
ATTR_COUNT: image_count,
|
||||
ATTR_USPS_IMAGE: config["image_name"],
|
||||
ATTR_IMAGE_PATH: config["image_output_path"],
|
||||
ATTR_GRID_IMAGE_NAME: config["image_name"].replace(".gif", "_grid.png"),
|
||||
}
|
||||
|
||||
async def process_batch(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
date: str,
|
||||
sensors: list[str],
|
||||
cache: EmailCache,
|
||||
since_date: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Process multiple USPS sensors in batch."""
|
||||
res = {}
|
||||
for sensor in sensors:
|
||||
res.update(await self.process(account, date, sensor, cache))
|
||||
|
||||
# Replicatecoordinator dict structure
|
||||
if sensor not in res:
|
||||
if ATTR_COUNT in res:
|
||||
res[sensor] = res[ATTR_COUNT]
|
||||
# Don't pop ATTR_COUNT because other things might need it, actually
|
||||
# coordinator used to pop it via explicit assignment
|
||||
|
||||
return res
|
||||
|
||||
async def _generate_mp4_video(self, path: str, name: str):
|
||||
"""Generate MP4 video from images."""
|
||||
await self.hass.async_add_executor_job(_generate_mp4, path, name)
|
||||
|
||||
async def _generate_grid_image(self, path: str, name: str, count: int):
|
||||
"""Generate grid image from images."""
|
||||
await self.hass.async_add_executor_job(generate_grid_img, path, name, count)
|
||||
|
||||
async def _process_usps_images(self, content: str, images: list) -> list:
|
||||
"""Process USPS images (placeholder and filtering)."""
|
||||
# Old USPS format: plain-text email body contained the filename as a reference.
|
||||
# New format is handled in _extract_usps_images on properly decoded HTML.
|
||||
if re.compile(r"\bimage-no-mailpieces?700\.jpg\b").search(content) is not None:
|
||||
placeholder = Path(__file__).parent.parent / "image-no-mailpieces700.jpg"
|
||||
placeholder_str = str(placeholder)
|
||||
if placeholder.exists() and placeholder_str not in images:
|
||||
images.append(placeholder_str)
|
||||
_LOGGER.debug(
|
||||
"Placeholder image found using: image-no-mailpieces700.jpg.",
|
||||
)
|
||||
|
||||
# Announcement images removal
|
||||
return self._remove_announcement_images(images)
|
||||
|
||||
def _remove_announcement_images(self, images: list) -> list:
|
||||
"""Remove announcement images."""
|
||||
return [
|
||||
el
|
||||
for el in images
|
||||
if not any(
|
||||
ignore in el
|
||||
for ignore in ["mailerProvidedImage", "ra_0", "Mail Attachment.txt"]
|
||||
)
|
||||
]
|
||||
|
||||
async def _generate_mail_image(
|
||||
self,
|
||||
images: list,
|
||||
path: str,
|
||||
name: str,
|
||||
duration: int,
|
||||
delete_list: list,
|
||||
):
|
||||
"""Generate animated GIF from mail images."""
|
||||
_LOGGER.debug("Resizing images to 724x320...")
|
||||
all_images = await self.hass.async_add_executor_job(
|
||||
resize_images,
|
||||
images,
|
||||
724,
|
||||
320,
|
||||
)
|
||||
delete_list.extend(all_images)
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Generating animated GIF")
|
||||
gif_path = str(Path(path) / name)
|
||||
await self.hass.async_add_executor_job(
|
||||
generate_delivery_gif,
|
||||
all_images,
|
||||
gif_path,
|
||||
duration * 1000,
|
||||
)
|
||||
_LOGGER.debug("Mail image generated.")
|
||||
except (OSError, ValueError) as err:
|
||||
_LOGGER.error("Error attempting to generate image: %s", err)
|
||||
|
||||
for image in delete_list:
|
||||
await self.hass.async_add_executor_job(
|
||||
cleanup_images,
|
||||
f"{Path(image).parent}/",
|
||||
Path(image).name,
|
||||
)
|
||||
|
||||
async def _copy_nomail_image(self, path: str, name: str, custom_img: str | None):
|
||||
"""Copy the 'no mail' placeholder image."""
|
||||
|
||||
def _prepare():
|
||||
if not Path(path).exists():
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
target = Path(path) / name
|
||||
if target.is_file():
|
||||
cleanup_images(path, name)
|
||||
src = custom_img or str(Path(__file__).parent.parent / "mail_none.gif")
|
||||
shutil.copyfile(src, str(target))
|
||||
|
||||
_LOGGER.debug("No mail found.")
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_prepare)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error attempting to copy image: %s", err)
|
||||
|
||||
def _get_usps_config(self) -> dict:
|
||||
"""Get USPS specific configuration."""
|
||||
return {
|
||||
"image_output_path": self.config.get("image_path"),
|
||||
"gif_duration": self.config.get(CONF_DURATION),
|
||||
"image_name": self.config.get("usps_image"),
|
||||
"gen_mp4": self.config.get(CONF_GENERATE_MP4),
|
||||
"custom_img": self.config.get(CONF_CUSTOM_IMG_FILE)
|
||||
or DEFAULT_CUSTOM_IMG_FILE,
|
||||
"gen_grid": self.config.get(CONF_GENERATE_GRID),
|
||||
}
|
||||
|
||||
async def _search_informed_delivery(self, account: IMAP4_SSL) -> tuple:
|
||||
"""Search for USPS Informed Delivery emails."""
|
||||
_LOGGER.debug("Attempting to find Informed Delivery mail")
|
||||
_LOGGER.debug("Informed delivery search date: %s", get_formatted_date())
|
||||
|
||||
forwarding_header = self.config.get(CONF_FORWARDING_HEADER, "")
|
||||
if forwarding_header and forwarding_header != "(none)":
|
||||
email_addresses = SENSOR_DATA[ATTR_USPS_MAIL][ATTR_EMAIL]
|
||||
else:
|
||||
forwarding_header = ""
|
||||
forwarded_emails = self.config.get("forwarded_emails", [])
|
||||
if isinstance(forwarded_emails, str):
|
||||
forwarded_emails = [
|
||||
e.strip() for e in forwarded_emails.split(",") if e.strip()
|
||||
]
|
||||
if forwarded_emails:
|
||||
email_addresses = (
|
||||
forwarded_emails + SENSOR_DATA[ATTR_USPS_MAIL][ATTR_EMAIL]
|
||||
)
|
||||
else:
|
||||
email_addresses = SENSOR_DATA[ATTR_USPS_MAIL][ATTR_EMAIL]
|
||||
|
||||
return await email_search(
|
||||
account=account,
|
||||
address=email_addresses,
|
||||
date=get_formatted_date(),
|
||||
subject=SENSOR_DATA[ATTR_USPS_MAIL][ATTR_SUBJECT][0],
|
||||
header=forwarding_header,
|
||||
)
|
||||
|
||||
async def _setup_image_directory(self, path: str) -> bool:
|
||||
"""Ensure image directory exists and is prepared."""
|
||||
if not await anyio.Path(path).is_dir():
|
||||
try:
|
||||
await anyio.Path(path).mkdir(parents=True, exist_ok=True)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error creating directory: %s", err)
|
||||
return False
|
||||
|
||||
# Clean up and setup overlays
|
||||
await self.hass.async_add_executor_job(cleanup_images, path)
|
||||
await self.hass.async_add_executor_job(copy_overlays, path)
|
||||
return True
|
||||
|
||||
async def _process_usps_email(
|
||||
self,
|
||||
account: IMAP4_SSL,
|
||||
num: str,
|
||||
image_output_path: str,
|
||||
image_count: int,
|
||||
images: list,
|
||||
cache: EmailCache | None = None,
|
||||
) -> tuple[int, list, str]:
|
||||
"""Process a single USPS Informed Delivery email."""
|
||||
if cache:
|
||||
msg_parts = (await cache.fetch(num, "(RFC822)"))[1]
|
||||
else:
|
||||
msg_parts = (await email_fetch(account, num, "(RFC822)"))[1]
|
||||
_LOGGER.debug("Processing email number: %s", num)
|
||||
all_content = ""
|
||||
for response_part in msg_parts:
|
||||
if isinstance(response_part, (bytes, bytearray)):
|
||||
all_content += str(response_part, "utf-8", errors="ignore")
|
||||
msg = email.message_from_bytes(response_part)
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/html":
|
||||
(image_count, images) = await self._extract_usps_images(
|
||||
part,
|
||||
image_output_path,
|
||||
image_count,
|
||||
images,
|
||||
)
|
||||
elif part.get_content_type() == "image/jpeg":
|
||||
(image_count, images) = await self._extract_jpeg_attachment(
|
||||
part,
|
||||
image_output_path,
|
||||
image_count,
|
||||
images,
|
||||
)
|
||||
return image_count, images, all_content
|
||||
|
||||
async def _extract_usps_images(
|
||||
self,
|
||||
part: email.message.Message,
|
||||
image_output_path: str,
|
||||
image_count: int,
|
||||
images: list,
|
||||
) -> tuple[int, list]:
|
||||
"""Extract images from an email part (HTML/Base64)."""
|
||||
payload = part.get_payload(decode=True)
|
||||
content = (
|
||||
payload.decode("utf-8", "ignore")
|
||||
if isinstance(payload, (bytes, bytearray))
|
||||
else str(payload)
|
||||
)
|
||||
|
||||
# New USPS format: unscanned mailpieces use a div with a specific id.
|
||||
# Check here on properly decoded HTML — raw RFC822 content is
|
||||
# quoted-printable encoded and soft line breaks could split the string.
|
||||
if "mailpiece-with-no-image-id" in content:
|
||||
placeholder = Path(__file__).parent.parent / "image-no-mailpieces700.jpg"
|
||||
placeholder_str = str(placeholder)
|
||||
if placeholder.exists() and placeholder_str not in images:
|
||||
images.append(placeholder_str)
|
||||
image_count += 1
|
||||
_LOGGER.debug(
|
||||
"Placeholder image found using: image-no-mailpieces700.jpg.",
|
||||
)
|
||||
|
||||
if "data:image/jpeg;base64" not in content:
|
||||
return image_count, images
|
||||
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
found_images = soup.find_all(id="mailpiece-image-src-id")
|
||||
|
||||
for image in found_images:
|
||||
filename = random_filename()
|
||||
img_data = str(image["src"]).split(",")[1]
|
||||
try:
|
||||
target_path = Path(image_output_path) / filename
|
||||
await self.hass.async_add_executor_job(
|
||||
io_save_file,
|
||||
target_path,
|
||||
base64.b64decode(img_data),
|
||||
)
|
||||
images.append(str(target_path))
|
||||
image_count += 1
|
||||
except (OSError, ValueError, TypeError) as err:
|
||||
_LOGGER.error("Error extracting image: %s", err)
|
||||
|
||||
return image_count, images
|
||||
|
||||
async def _extract_jpeg_attachment(
|
||||
self,
|
||||
part: email.message.Message,
|
||||
image_output_path: str,
|
||||
image_count: int,
|
||||
images: list,
|
||||
) -> tuple[int, list]:
|
||||
"""Extract image from JPEG attachment."""
|
||||
_LOGGER.debug("Extracting image from email attachment")
|
||||
filename = part.get_filename()
|
||||
junkmail = ["mailer", "content", "package"]
|
||||
if filename is None:
|
||||
return image_count, images
|
||||
if any(junk in filename for junk in junkmail):
|
||||
return image_count, images
|
||||
|
||||
try:
|
||||
target_path = Path(image_output_path) / filename
|
||||
await self.hass.async_add_executor_job(
|
||||
io_save_file,
|
||||
target_path,
|
||||
part.get_payload(decode=True),
|
||||
)
|
||||
images.append(str(target_path))
|
||||
image_count += 1
|
||||
except OSError as err:
|
||||
_LOGGER.critical("Error opening filepath: %s", err)
|
||||
|
||||
return image_count, images
|
||||
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of Mail and Packages is allowed.",
|
||||
"reconfigure_successful": "Reconfigure Successful"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect or login to the mail server. Please check the log for details.",
|
||||
"invalid_path": "Please store the images in another directory.",
|
||||
"ffmpeg_not_found": "Generate MP4 requires ffmpeg",
|
||||
"amazon_domain": "Invalid forwarding email address.",
|
||||
"file_not_found": "Image file not found",
|
||||
"invalid_email_format": "Invalid email address format.",
|
||||
"missing_forwarded_emails": "Missing forwarded email addresses. You may enter '(none)' to clear this setting.",
|
||||
"path_not_found": "Directory not found",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Please enter the connection information of your mail server.",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"verify_ssl": "Verify SSL Cert",
|
||||
"imap_security": "IMAP Security"
|
||||
},
|
||||
"title": "Mail and Packages (Step 1 of 2)"
|
||||
},
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Mail Folder",
|
||||
"resources": "Sensors List",
|
||||
"scan_interval": "Scanning Interval (minutes, minimum 5)",
|
||||
"image_path": "Image Path",
|
||||
"gif_duration": "Image Duration (seconds)",
|
||||
"image_security": "Random Image Filename",
|
||||
"imap_timeout": "Mailbox scan time limit (seconds, minimum 10)",
|
||||
"generate_grid": "Create image grid for LLM vision models",
|
||||
"generate_mp4": "Create mp4 from images",
|
||||
"allow_external": "Create image for notification apps",
|
||||
"custom_img": "Use custom USPS 'no mail' image?",
|
||||
"amazon_custom_img": "Use custom 'no Amazon delivery' image?",
|
||||
"ups_custom_img": "Use custom 'no UPS delivery' image?",
|
||||
"walmart_custom_img": "Use custom 'no Walmart delivery' image?",
|
||||
"fedex_custom_img": "Use custom 'no FedEx delivery' image?",
|
||||
"post_de_custom_img": "Use custom Post DE 'no mail' image?",
|
||||
"allow_forwarded_emails": "Allow forwarded emails in addition to a service's default (e.g. no-reply@usps.com)"
|
||||
},
|
||||
"data_description": {
|
||||
"imap_timeout": "Covers the entire scan — login plus every per-carrier IMAP search — not just connecting. Raise this if large mailboxes time out; lower it to fail faster."
|
||||
},
|
||||
"description": "Finish the configuration by customizing the following based on your email structure and Home Assistant installation.\n\nIf using Amazon forwarded emails please separate each address with a comma.",
|
||||
"title": "Mail and Packages (Step 2 of 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Path to custom USPS image: (ie: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)",
|
||||
"post_de_custom_img_file": "Path to custom Post DE image: (ie: images/my_custom_no_post_de.gif)"
|
||||
},
|
||||
"description": "Enter the path and file name to your custom no delivery images below.\n\nExample: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Step 3 of 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon forwarded email addresses",
|
||||
"amazon_days": "Days back to check for Amazon emails"
|
||||
},
|
||||
"description": "Please enter the domain Amazon sends email's from (ie: amazon.com or amazon.de)\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.",
|
||||
"title": "Amazon Settings"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarded_emails": "Forwarded Email Addresses"
|
||||
},
|
||||
"description": "By default, the Mail and Packages integration only searches for messages matching a service's address.\n\nIf you want to use forwarded emails, please separate each address messages will be coming from with a comma or enter '(none)' to clear this setting.",
|
||||
"title": "Forwarded Email Addresses"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"imap_security": "IMAP Security",
|
||||
"verify_ssl": "Verify SSL Cert"
|
||||
},
|
||||
"description": "Please enter the connection information of your mail server.",
|
||||
"title": "Mail and Packages (Step 1 of 2)"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Mail Folder",
|
||||
"scan_interval": "Scanning Interval (minutes, minimum 5)",
|
||||
"image_path": "Image Path",
|
||||
"gif_duration": "Image Duration (seconds)",
|
||||
"image_security": "Random Image Filename",
|
||||
"generate_grid": "Create image grid for LLM vision models",
|
||||
"generate_mp4": "Create mp4 from images",
|
||||
"resources": "Sensors List",
|
||||
"imap_timeout": "Mailbox scan time limit (seconds, minimum 10)",
|
||||
"allow_external": "Create image for notification apps",
|
||||
"custom_img": "Use custom USPS 'no mail' image?",
|
||||
"amazon_custom_img": "Use custom 'no Amazon delivery' image?",
|
||||
"ups_custom_img": "Use custom 'no UPS delivery' image?",
|
||||
"walmart_custom_img": "Use custom 'no Walmart delivery' image?",
|
||||
"fedex_custom_img": "Use custom 'no FedEx delivery' image?",
|
||||
"post_de_custom_img": "Use custom Post DE 'no mail' image?",
|
||||
"allow_forwarded_emails": "Allow forwarded emails in addition to a service's default (e.g. no-reply@usps.com)"
|
||||
},
|
||||
"data_description": {
|
||||
"imap_timeout": "Covers the entire scan — login plus every per-carrier IMAP search — not just connecting. Raise this if large mailboxes time out; lower it to fail faster."
|
||||
},
|
||||
"description": "Finish the configuration by customizing the following based on your email structure and Home Assistant installation.\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.",
|
||||
"title": "Mail and Packages (Step 2 of 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Path to custom USPS image: (i.e.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)",
|
||||
"post_de_custom_img_file": "Path to custom Post DE image: (i.e.: images/my_custom_no_post_de.gif)"
|
||||
},
|
||||
"description": "Enter the path and file name to your custom no delivery images below.\n\nExample: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Step 3 of 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon forwarded email addresses",
|
||||
"amazon_days": "Days back to check for Amazon emails"
|
||||
},
|
||||
"description": "Please enter the domain Amazon sends email's from (ie: amazon.com or amazon.de)\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.",
|
||||
"title": "Amazon Settings"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Directory to store images"
|
||||
},
|
||||
"description": "Please enter the directory you'd like your images to be stored in.\nThe default is auto populated.",
|
||||
"title": "Image storage location"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Directory to store images"
|
||||
},
|
||||
"description": "Please enter the directory you'd like your images to be stored in.\nThe default is auto populated.",
|
||||
"title": "Image storage location"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarded_emails": "Forwarded Email Addresses"
|
||||
},
|
||||
"description": "By default, the Mail and Packages integration only searches for messages matching a service's address.\n\nIf you want to use forwarded emails, please separate each address messages will be coming from with a comma or enter '(none)' to clear this setting.",
|
||||
"title": "Forwarded Email Addresses"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Només es permet una única configuració de correu i paquets.",
|
||||
"reconfigure_successful": "Reconfiguració exitosa"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No es pot connectar o iniciar sessió al servidor de correu. Consulteu el registre per obtenir més detalls.",
|
||||
"invalid_path": "Deseu les imatges en un altre directori.",
|
||||
"ffmpeg_not_found": "Generar MP4 requereix ffmpeg",
|
||||
"amazon_domain": "Adreça de correu electrònic de reenviament no vàlida.",
|
||||
"file_not_found": "No s'ha trobat el fitxer d'imatge",
|
||||
"invalid_email_format": "Format d'adreça de correu electrònic no vàlid.",
|
||||
"path_not_found": "Directori no trobat",
|
||||
"missing_forwarded_emails": "Falten les adreces de correu electrònic reenviades. Podeu introduir \"(none)\" per esborrar aquesta configuració.",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Carpeta de correu",
|
||||
"resources": "Llista de sensors",
|
||||
"scan_interval": "Interval d'exploració (minuts)",
|
||||
"image_path": "Camí de la imatge",
|
||||
"gif_duration": "Durada de la imatge (segons)",
|
||||
"image_security": "Nom del fitxer d'imatge aleatòria",
|
||||
"imap_timeout": "Temps en segons abans de l'expiració de la connexió (segons, mínim 10)",
|
||||
"generate_mp4": "Crea mp4 a partir d'imatges",
|
||||
"allow_external": "Crea una imatge per a aplicacions de notificacions",
|
||||
"custom_img": "Utilitza la imatge personalitzada USPS 'no hi ha correu'?",
|
||||
"amazon_custom_img": "Utilitza la imatge personalitzada Amazon 'no hi ha lliurament'?",
|
||||
"ups_custom_img": "Utilitza la imatge personalitzada UPS 'no hi ha lliurament'?",
|
||||
"walmart_custom_img": "Utilitza la imatge personalitzada Walmart 'no hi ha lliurament'?",
|
||||
"fedex_custom_img": "Utilitza la imatge personalitzada FedEx 'no hi ha lliurament'?",
|
||||
"generic_custom_img": "Utilitza la imatge personalitzada genèrica 'no hi ha lliurament'?",
|
||||
"generate_grid": "Crea una quadrícula d'imatges per a models de visió LLM",
|
||||
"allow_forwarded_emails": "Permet els correus electrònics reenviats a més del valor predeterminat d'un servei (p. ex., no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Finalitzeu la configuració personalitzant la següent en funció de la vostra instal·lació de correu electrònic i la instal·lació d'assistència a casa. \n\n Per obtenir més informació sobre les opcions [Integració de paquets i correus] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), reviseu les opcions [configuració, plantilles , secció i automatitzacions] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) a GitHub.",
|
||||
"title": "Correu i paquets (pas 2 de 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Camí a la imatge personalitzada: (és a dir: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Introduïu el camí i el nom del fitxer de la vostra imatge personalitzada de no correu a continuació.\n\nExemple: imatges/custom_nomail.gif",
|
||||
"title": "Correu i paquets (Pas 3 de 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domini d'Amazon",
|
||||
"amazon_fwds": "Amazon ha reenviat les adreces de correu electrònic",
|
||||
"amazon_days": "Dies per comprovar els correus electrònics d'Amazon"
|
||||
},
|
||||
"description": "Introduïu el domini des d'on Amazon envia correus electrònics (és a dir: amazon.com o amazon.de)\n\nSi utilitzeu correus electrònics reenviats d'Amazon, separeu cada adreça amb una coma o introduïu (none) per esborrar aquesta configuració.\n\nNota: si heu configurat una capçalera de reenviament en el pas anterior, el camp d'adreça de reenviament no apareixerà — la capçalera gestiona la correspondència de correus electrònics d'Amazon automàticament.",
|
||||
"title": "Configuració d'Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Carpeta de correu",
|
||||
"scan_interval": "Interval d'exploració (minuts)",
|
||||
"image_path": "Camí de la imatge",
|
||||
"gif_duration": "Durada de la imatge (segons)",
|
||||
"image_security": "Nom del fitxer d'imatge aleatòria",
|
||||
"generate_mp4": "Crea mp4 a partir d'imatges",
|
||||
"resources": "Llista de sensors",
|
||||
"imap_timeout": "Temps en segons abans de l'expiració de la connexió (segons, mínim 10)",
|
||||
"allow_external": "Crea una imatge per a aplicacions de notificacions",
|
||||
"custom_img": "Utilitza la imatge personalitzada USPS 'no hi ha correu'?",
|
||||
"amazon_custom_img": "Utilitza la imatge personalitzada Amazon 'no hi ha lliurament'?",
|
||||
"ups_custom_img": "Utilitza la imatge personalitzada UPS 'no hi ha lliurament'?",
|
||||
"walmart_custom_img": "Utilitza la imatge personalitzada Walmart 'no hi ha lliurament'?",
|
||||
"fedex_custom_img": "Utilitza la imatge personalitzada FedEx 'no hi ha lliurament'?",
|
||||
"generic_custom_img": "Utilitza la imatge personalitzada genèrica 'no hi ha lliurament'?",
|
||||
"generate_grid": "Crea una quadrícula d'imatges per a models de visió LLM",
|
||||
"allow_forwarded_emails": "Permet els correus electrònics reenviats a més del valor predeterminat d'un servei (p. ex., no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Acabeu la configuració personalitzant el següent en funció de l'estructura del vostre correu electrònic i de la instal·lació de Home Assistant.\n\nPer obtenir més detalls sobre les opcions d'[integració de correu i paquets](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), consulteu la [secció de configuració, plantilles i automatitzacions](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) a GitHub.\n\nSi utilitzeu correus electrònics reenviats d'Amazon, separeu cada adreça amb una coma o introduïu (none) per esborrar aquesta configuració.",
|
||||
"title": "Correu i paquets (pas 2 de 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Camí a la imatge personalitzada: (p. ex.: imatges/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Introduïu el camí i el nom del fitxer de la vostra imatge personalitzada de no correu a continuació.\n\nExemple: imatges/custom_nomail.gif",
|
||||
"title": "Correu i paquets (Pas 3 de 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domini d'Amazon",
|
||||
"amazon_fwds": "Amazon ha reenviat les adreces de correu electrònic",
|
||||
"amazon_days": "Dies per comprovar els correus electrònics d'Amazon"
|
||||
},
|
||||
"description": "Introduïu el domini des d'on Amazon envia correus electrònics (és a dir: amazon.com o amazon.de)\n\nSi utilitzeu correus electrònics reenviats d'Amazon, separeu cada adreça amb una coma o introduïu (none) per esborrar aquesta configuració.\n\nNota: si heu configurat una capçalera de reenviament en el pas anterior, el camp d'adreça de reenviament no apareixerà — la capçalera gestiona la correspondència de correus electrònics d'Amazon automàticament.",
|
||||
"title": "Configuració d'Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Directori per emmagatzemar imatges"
|
||||
},
|
||||
"description": "Introduïu el directori on voleu que es desin les vostres imatges.\nPer defecte, es completa automàticament.",
|
||||
"title": "Ubicació de l'emmagatzematge d'imatges"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Directori per emmagatzemar imatges"
|
||||
},
|
||||
"description": "Introduïu el directori on voleu que es desin les vostres imatges.\nPer defecte, es completa automàticament.",
|
||||
"title": "Ubicació de l'emmagatzematge d'imatges"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Capçalera de reenviament (p. ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Adreces de correu electrònic reenviades"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si el vostre servei de reenviament inclou el remitent original en una capçalera, introduïu aquí el nom de la capçalera. Això té prioritat sobre la llista d'adreces a continuació. Introduïu '(none)' o deixeu-ho en blanc per utilitzar la llista d'adreces.",
|
||||
"forwarded_emails": "Llista d'adreces de reenviament separades per comes des de les quals arribaran els missatges. Deixeu '(none)' si feu servir la capçalera de reenviament de dalt."
|
||||
},
|
||||
"description": "Trieu com fer coincidir els correus electrònics reenviats.\n\n**Opció 1 — Capçalera de reenviament**: Si el vostre servei (p. ex. SimpleLogin) inclou el remitent original en una capçalera personalitzada, introduïu el nom d'aquesta capçalera. Mail and Packages l'utilitzarà per fer coincidir automàticament les adreces dels transportistes.\n\n**Opció 2 — Llista d'adreces**: Introduïu les adreces de reenviament que el vostre servei ha assignat a cada transportista, separades per comes.",
|
||||
"title": "Configuració dels correus electrònics reenviats"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Capçalera de reenviament (p. ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Adreces de correu electrònic reenviades"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si el vostre servei de reenviament inclou el remitent original en una capçalera, introduïu aquí el nom de la capçalera. Això té prioritat sobre la llista d'adreces a continuació. Introduïu '(none)' o deixeu-ho en blanc per utilitzar la llista d'adreces.",
|
||||
"forwarded_emails": "Llista d'adreces de reenviament separades per comes des de les quals arribaran els missatges. Deixeu '(none)' si feu servir la capçalera de reenviament de dalt."
|
||||
},
|
||||
"description": "Trieu com fer coincidir els correus electrònics reenviats.\n\n**Opció 1 — Capçalera de reenviament**: Si el vostre servei (p. ex. SimpleLogin) inclou el remitent original en una capçalera personalitzada, introduïu el nom d'aquesta capçalera. Mail and Packages l'utilitzarà per fer coincidir automàticament les adreces dels transportistes.\n\n**Opció 2 — Llista d'adreces**: Introduïu les adreces de reenviament que el vostre servei ha assignat a cada transportista, separades per comes.",
|
||||
"title": "Configuració dels correus electrònics reenviats"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Introduïu la informació de connexió del vostre servidor de correu.",
|
||||
"data": {
|
||||
"host": "Amfitrió",
|
||||
"password": "Contrasenya",
|
||||
"port": "Port",
|
||||
"username": "Nom d'usuari",
|
||||
"verify_ssl": "Verifica el certificat SSL",
|
||||
"imap_security": "Seguretat IMAP"
|
||||
},
|
||||
"title": "Correu i paquets (pas 1 de 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Amfitrió",
|
||||
"password": "Contrasenya",
|
||||
"port": "Port",
|
||||
"username": "Nom d'usuari",
|
||||
"imap_security": "Seguretat IMAP",
|
||||
"verify_ssl": "Verifica el certificat SSL"
|
||||
},
|
||||
"description": "Introduïu la informació de connexió del vostre servidor de correu.",
|
||||
"title": "Correu i paquets (pas 1 de 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Je povolena pouze jedna konfigurace Mail and Packages.",
|
||||
"reconfigure_successful": "Rekonfigurace úspěšná"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nelze se připojit nebo přihlásit k poštovnímu serveru. Podrobnosti naleznete v logu.",
|
||||
"invalid_path": "Prosím uložte obrázky do jiného adresáře.",
|
||||
"ffmpeg_not_found": "Generování MP4 vyžaduje ffmpeg",
|
||||
"amazon_domain": "Neplatná e-mailová adresa pro přeposílání.",
|
||||
"file_not_found": "Soubor obrázku nebyl nalezen",
|
||||
"missing_forwarded_emails": "Chybí přeposílané e-mailové adresy. Můžete zadat '(none)' pro vymazání tohoto nastavení.",
|
||||
"scan_too_low": "Interval skenování je příliš nízký (minimum 5)",
|
||||
"timeout_too_low": "IMAP timeout je příliš nízký (minimum 10)",
|
||||
"invalid_email_format": "Invalid email address format.",
|
||||
"path_not_found": "Directory not found",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Poštovní složka",
|
||||
"scan_interval": "Interval skenování (minuty)",
|
||||
"image_path": "Cesta obrázku",
|
||||
"gif_duration": "Trvání obrázku (sekund)",
|
||||
"image_security": "Jméno náhodného obrázku",
|
||||
"generate_mp4": "Vytvořit mp4 z obrázku",
|
||||
"resources": "Seznam senzorů",
|
||||
"imap_timeout": "Čas v sekundách před vypršením časového limitu připojení (sekund, minimum 10)",
|
||||
"amazon_fwds": "Amazon předal e-mailové adresy",
|
||||
"allow_external": "Vytvořte obrázek pro oznamovací aplikaci",
|
||||
"amazon_days": "Dny zpět ke kontrole e-mailů Amazon",
|
||||
"custom_img": "Použít vlastní obrázek USPS 'bez pošty'?",
|
||||
"amazon_custom_img": "Použít vlastní obrázek Amazon 'bez dodání'?",
|
||||
"ups_custom_img": "Použít vlastní obrázek UPS 'bez dodání'?",
|
||||
"walmart_custom_img": "Použít vlastní obrázek Walmart 'bez dodání'?",
|
||||
"fedex_custom_img": "Použít vlastní obrázek FedEx 'bez dodání'?",
|
||||
"generic_custom_img": "Použít vlastní obrázek obecný 'bez dodání'?",
|
||||
"allow_forwarded_emails": "Povolit přeposílané e-maily kromě výchozí hodnoty služby (např. no-reply@usps.com)",
|
||||
"generate_grid": "Create image grid for LLM vision models",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Dokončete konfiguraci přizpůsobením následujících položek na základě struktury e-mailu a instalace Home Assistant.\n\nPodrobnosti o [Mail and Packages integration](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) můžete zkontrolovat na [configurace, styly a automatizace](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHubu.",
|
||||
"title": "Mail and Packages (Krok 2 ze 3)"
|
||||
},
|
||||
"options_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Cesta k vlastnímu obrázku: (např.: images/my_custom_no_mail.jpg)"
|
||||
},
|
||||
"description": "Níže zadejte cestu a název souboru k vlastnímu obrázku bez pošty.\n\nPříklad: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Krok 3 ze 3)"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Hlavička přesměrování (např. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Přeposílané e-mailové adresy"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Pokud vaše přeposílací služba zahrnuje původního odesílatele do záhlaví, zadejte zde název záhlaví. To má přednost před níže uvedeným seznamem adres. Zadejte '(none)' nebo nechte prázdné, chcete-li místo toho použít seznam adres.",
|
||||
"forwarded_emails": "Seznam přeposílacích adres oddělených čárkami, ze kterých budou přicházet zprávy. Ponechejte '(none)', pokud používáte výše uvedenou hlavičku přesměrování."
|
||||
},
|
||||
"description": "Vyberte způsob porovnávání přeposlaných e-mailů.\n\n**Možnost 1 — Hlavička přesměrování**: Pokud vaše služba (např. SimpleLogin) zahrnuje původního odesílatele do vlastního záhlaví, zadejte název tohoto záhlaví. Mail and Packages ho použije k automatickému přiřazení adres dopravců.\n\n**Možnost 2 — Seznam adres**: Zadejte přeposílací adresy, které vaše služba přidělila každému dopravci, oddělené čárkami.",
|
||||
"title": "Nastavení přeposlaných e-mailů"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Hlavička přesměrování (např. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Přeposílané e-mailové adresy"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Pokud vaše přeposílací služba zahrnuje původního odesílatele do záhlaví, zadejte zde název záhlaví. To má přednost před níže uvedeným seznamem adres. Zadejte '(none)' nebo nechte prázdné, chcete-li místo toho použít seznam adres.",
|
||||
"forwarded_emails": "Seznam přeposílacích adres oddělených čárkami, ze kterých budou přicházet zprávy. Ponechejte '(none)', pokud používáte výše uvedenou hlavičku přesměrování."
|
||||
},
|
||||
"description": "Vyberte způsob porovnávání přeposlaných e-mailů.\n\n**Možnost 1 — Hlavička přesměrování**: Pokud vaše služba (např. SimpleLogin) zahrnuje původního odesílatele do vlastního záhlaví, zadejte název tohoto záhlaví. Mail and Packages ho použije k automatickému přiřazení adres dopravců.\n\n**Možnost 2 — Seznam adres**: Zadejte přeposílací adresy, které vaše služba přidělila každému dopravci, oddělené čárkami.",
|
||||
"title": "Nastavení přeposlaných e-mailů"
|
||||
},
|
||||
"imap_config": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Heslo",
|
||||
"port": "Port",
|
||||
"username": "Uživatelské jméno",
|
||||
"verify_ssl": "Verify SSL Cert",
|
||||
"imap_security": "IMAP Security"
|
||||
},
|
||||
"description": "Zadejte prosím informace o připojení vašeho poštovního serveru.",
|
||||
"title": "Mail and Packages (Krok 1 ze 3)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Path to custom USPS image: (ie: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Enter the path and file name to your custom no delivery images below.\n\nExample: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Step 3 of 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon forwarded email addresses",
|
||||
"amazon_days": "Days back to check for Amazon emails"
|
||||
},
|
||||
"description": "Please enter the domain Amazon sends email's from (ie: amazon.com or amazon.de)\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.\n\nNote: if you configured a forwarding header in the previous step, the forwarding address field will not appear — the header handles Amazon email matching automatically.",
|
||||
"title": "Amazon Settings"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"imap_security": "IMAP Security",
|
||||
"verify_ssl": "Verify SSL Cert"
|
||||
},
|
||||
"description": "Please enter the connection information of your mail server.",
|
||||
"title": "Mail and Packages (Step 1 of 2)"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Mail Folder",
|
||||
"scan_interval": "Scanning Interval (minutes, minimum 5)",
|
||||
"image_path": "Image Path",
|
||||
"gif_duration": "Image Duration (seconds)",
|
||||
"image_security": "Random Image Filename",
|
||||
"generate_grid": "Create image grid for LLM vision models",
|
||||
"generate_mp4": "Create mp4 from images",
|
||||
"resources": "Sensors List",
|
||||
"imap_timeout": "Time in seconds before connection timeout (seconds, minimum 10)",
|
||||
"allow_external": "Create image for notification apps",
|
||||
"custom_img": "Use custom USPS 'no mail' image?",
|
||||
"amazon_custom_img": "Use custom 'no Amazon delivery' image?",
|
||||
"ups_custom_img": "Use custom 'no UPS delivery' image?",
|
||||
"walmart_custom_img": "Use custom 'no Walmart delivery' image?",
|
||||
"fedex_custom_img": "Use custom 'no FedEx delivery' image?",
|
||||
"allow_forwarded_emails": "Allow forwarded emails in addition to a service's default (e.g. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Finish the configuration by customizing the following based on your email structure and Home Assistant installation.\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.",
|
||||
"title": "Mail and Packages (Step 2 of 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Path to custom USPS image: (i.e.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Enter the path and file name to your custom no delivery images below.\n\nExample: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Step 3 of 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon forwarded email addresses",
|
||||
"amazon_days": "Days back to check for Amazon emails"
|
||||
},
|
||||
"description": "Please enter the domain Amazon sends email's from (ie: amazon.com or amazon.de)\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.\n\nNote: if you configured a forwarding header in the previous step, the forwarding address field will not appear — the header handles Amazon email matching automatically.",
|
||||
"title": "Amazon Settings"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Directory to store images"
|
||||
},
|
||||
"description": "Please enter the directory you'd like your images to be stored in.\nThe default is auto populated.",
|
||||
"title": "Image storage location"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Directory to store images"
|
||||
},
|
||||
"description": "Please enter the directory you'd like your images to be stored in.\nThe default is auto populated.",
|
||||
"title": "Image storage location"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
},
|
||||
"title": "Mail and Packages"
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"cannot_connect": "Nelze se připojit nebo přihlásit k poštovnímu serveru. Podrobnosti naleznete v logu.",
|
||||
"invalid_path": "Prosím uložte obrázky do jiného adresáře.",
|
||||
"ffmpeg_not_found": "Generování MP4 vyžaduje ffmpeg",
|
||||
"amazon_domain": "Neplatná e-mailová adresa pro přeposílání.",
|
||||
"file_not_found": "Soubor obrázku nebyl nalezen",
|
||||
"missing_forwarded_emails": "Chybí přeposílané e-mailové adresy. Můžete zadat '(none)' pro vymazání tohoto nastavení.",
|
||||
"scan_too_low": "Interval skenování je příliš nízký (minimum 5)",
|
||||
"timeout_too_low": "IMAP timeout je příliš nízký (minimum 10)"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Heslo",
|
||||
"port": "Port",
|
||||
"username": "Uživatelské jméno"
|
||||
},
|
||||
"description": "Zadejte prosím informace o připojení vašeho poštovního serveru.",
|
||||
"title": "Mail and Packages (Krok 1 ze 3)"
|
||||
},
|
||||
"options_2": {
|
||||
"data": {
|
||||
"folder": "Poštovní složka",
|
||||
"scan_interval": "Interval skenování (minuty)",
|
||||
"image_path": "Cesta obrázku",
|
||||
"gif_duration": "Trvání obrázku (sekund)",
|
||||
"image_security": "Jméno náhodného obrázku",
|
||||
"generate_mp4": "Vytvořit mp4 z obrázku",
|
||||
"resources": "Seznam senzorů",
|
||||
"imap_timeout": "Čas v sekundách před vypršením časového limitu připojení (sekund, minimum 10)",
|
||||
"amazon_fwds": "Amazon předal e-mailové adresy",
|
||||
"allow_external": "Vytvořte obrázek pro oznamovací aplikaci",
|
||||
"amazon_days": "Dny zpět ke kontrole e-mailů Amazon",
|
||||
"custom_img": "Použít vlastní obrázek USPS 'bez pošty'?",
|
||||
"amazon_custom_img": "Použít vlastní obrázek Amazon 'bez dodání'?",
|
||||
"ups_custom_img": "Použít vlastní obrázek UPS 'bez dodání'?",
|
||||
"walmart_custom_img": "Použít vlastní obrázek Walmart 'bez dodání'?",
|
||||
"generic_custom_img": "Použít vlastní obrázek obecný 'bez dodání'?",
|
||||
"allow_forwarded_emails": "Povolit přeposílané e-maily kromě výchozí hodnoty služby (např. no-reply@usps.com)"
|
||||
},
|
||||
"description": "Dokončete konfiguraci přizpůsobením následujících položek na základě struktury e-mailu a instalace Home Assistant.\n\nPodrobnosti o [Mail and Packages integration](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) můžete zkontrolovat na [configurace, styly a automatizace](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHubu.",
|
||||
"title": "Mail and Packages (Krok 2 ze 3)"
|
||||
},
|
||||
"options_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Cesta k vlastnímu obrázku: (např.: images/my_custom_no_mail.jpg)"
|
||||
},
|
||||
"description": "Níže zadejte cestu a název souboru k vlastnímu obrázku bez pošty.\n\nPříklad: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Krok 3 ze 3)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Mail and Packages",
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Es ist nur eine einzige Konfiguration von Mail und Paketen zulässig.",
|
||||
"reconfigure_successful": "Neukonfiguration erfolgreich"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Es kann keine Verbindung zum Mailserver hergestellt oder angemeldet werden. Bitte überprüfen Sie das Protokoll für Details.",
|
||||
"invalid_path": "Bitte speichern Sie die Bilder in einem anderen Verzeichnis.",
|
||||
"ffmpeg_not_found": "MP4 erstellen erfordert ffmpeg",
|
||||
"amazon_domain": "Ungültige Weiterleitungs-E-Mail-Adresse.",
|
||||
"file_not_found": "Bilddatei nicht gefunden",
|
||||
"invalid_email_format": "Ungültiges E-Mail-Adressformat.",
|
||||
"missing_forwarded_emails": "Fehlende weitergeleitete E-Mail-Adressen. Sie können '(none)' eingeben, um diese Einstellung zu löschen.",
|
||||
"path_not_found": "Verzeichnis nicht gefunden",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "E-Mail-Ordner",
|
||||
"resources": "Sensoren Liste",
|
||||
"scan_interval": "Scanintervall (Minuten)",
|
||||
"image_path": "Bildpfad",
|
||||
"gif_duration": "Bilddauer (Sekunden)",
|
||||
"image_security": "Zufälliger Bilddateiname",
|
||||
"imap_timeout": "Zeit in Sekunden bis zum Timeout der Verbindung (Sekunden, mindestens 10)",
|
||||
"generate_mp4": "Erstellen Sie mp4 aus Bildern",
|
||||
"allow_external": "Bild für Benachrichtigungs-Apps erstellen",
|
||||
"custom_img": "Benutzerdefiniertes USPS 'keine Post' Bild verwenden?",
|
||||
"amazon_custom_img": "Benutzerdefiniertes Amazon 'keine Lieferung' Bild verwenden?",
|
||||
"ups_custom_img": "Benutzerdefiniertes UPS 'keine Lieferung' Bild verwenden?",
|
||||
"walmart_custom_img": "Benutzerdefiniertes Walmart 'keine Lieferung' Bild verwenden?",
|
||||
"fedex_custom_img": "Benutzerdefiniertes FedEx 'keine Lieferung' Bild verwenden?",
|
||||
"generic_custom_img": "Benutzerdefiniertes generisches 'keine Lieferung' Bild verwenden?",
|
||||
"post_de_custom_img": "Benutzerdefiniertes Post DE 'keine Post' Bild verwenden?",
|
||||
"generate_grid": "Erstellen Sie ein Bildraster für LLM-Vision-Modelle",
|
||||
"allow_forwarded_emails": "Weitergeleitete E-Mails zusätzlich zu den Standardwerten eines Dienstes zulassen (z. B. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Beenden Sie die Konfiguration, indem Sie Folgendes basierend auf Ihrer E-Mail-Struktur und der Installation von Home Assistant anpassen. \n\n Weitere Informationen zu den Optionen [Mail- und Paketintegration] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) finden Sie in den [Konfiguration, Vorlagen und Abschnitt Automatisierungen] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) auf GitHub.",
|
||||
"title": "Briefe und Pakete (Schritt 2 von 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Pfad zum benutzerdefinierten Bild: (z. B.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)",
|
||||
"post_de_custom_img_file": "Pfad zum benutzerdefinierten Post DE-Bild: (z. B.: images/my_custom_no_post_de.gif)"
|
||||
},
|
||||
"description": "Geben Sie unten den Pfad und den Dateinamen zu Ihrem benutzerdefinierten Keine-Mail-Bild ein.\n\nBeispiel: images/custom_nomail.gif",
|
||||
"title": "Post und Pakete (Schritt 3 von 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon-Domain",
|
||||
"amazon_fwds": "Amazon hat E-Mail-Adressen weitergeleitet",
|
||||
"amazon_days": "Tage zurück, um nach Amazon-E-Mails zu suchen"
|
||||
},
|
||||
"description": "Bitte geben Sie die Domain ein, von der Amazon E-Mails sendet (z.B.: amazon.com oder amazon.de)\n\nWenn Sie weitergeleitete E-Mails von Amazon verwenden, trennen Sie bitte jede Adresse mit einem Komma oder geben Sie (keine) ein, um diese Einstellung zu löschen.\n\nHinweis: Wenn Sie im vorherigen Schritt einen Weiterleitungs-Header konfiguriert haben, wird das Feld für die Weiterleitungsadresse nicht angezeigt — der Header übernimmt die Zuordnung von Amazon-E-Mails automatisch.",
|
||||
"title": "Amazon Einstellungen"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "E-Mail-Ordner",
|
||||
"scan_interval": "Scanintervall (Minuten)",
|
||||
"image_path": "Bildpfad",
|
||||
"gif_duration": "Bilddauer (Sekunden)",
|
||||
"image_security": "Zufälliger Bilddateiname",
|
||||
"generate_mp4": "Erstellen Sie mp4 aus Bildern",
|
||||
"resources": "Sensoren Liste",
|
||||
"imap_timeout": "Zeit in Sekunden bis zum Timeout der Verbindung (Sekunden, mindestens 10)",
|
||||
"allow_external": "Bild für Benachrichtigungs-Apps erstellen",
|
||||
"custom_img": "Benutzerdefiniertes USPS 'keine Post' Bild verwenden?",
|
||||
"amazon_custom_img": "Benutzerdefiniertes Amazon 'keine Lieferung' Bild verwenden?",
|
||||
"ups_custom_img": "Benutzerdefiniertes UPS 'keine Lieferung' Bild verwenden?",
|
||||
"walmart_custom_img": "Benutzerdefiniertes Walmart 'keine Lieferung' Bild verwenden?",
|
||||
"fedex_custom_img": "Benutzerdefiniertes FedEx 'keine Lieferung' Bild verwenden?",
|
||||
"generic_custom_img": "Benutzerdefiniertes generisches 'keine Lieferung' Bild verwenden?",
|
||||
"post_de_custom_img": "Benutzerdefiniertes Post DE 'keine Post' Bild verwenden?",
|
||||
"generate_grid": "Erstellen Sie ein Bildraster für LLM-Vision-Modelle",
|
||||
"allow_forwarded_emails": "Weitergeleitete E-Mails zusätzlich zu den Standardwerten eines Dienstes zulassen (z. B. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Schließen Sie die Konfiguration ab, indem Sie das Folgende an Ihre E-Mail-Struktur und Home Assistant-Installation anpassen.\n\nWeitere Informationen zu den [Mail und Packages Integration](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) Optionen finden Sie im [Konfigurations-, Vorlagen- und Automatisierungsabschnitt](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) auf GitHub.\n\nWenn Sie weitergeleitete E-Mails von Amazon verwenden, trennen Sie bitte jede Adresse durch ein Komma oder geben Sie (keine) ein, um diese Einstellung zu löschen.",
|
||||
"title": "Briefe und Pakete (Schritt 2 von 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Pfad zum benutzerdefinierten Bild: (z.B.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)",
|
||||
"post_de_custom_img_file": "Pfad zum benutzerdefinierten Post DE-Bild: (z. B.: images/my_custom_no_post_de.gif)"
|
||||
},
|
||||
"description": "Geben Sie unten den Pfad und den Dateinamen zu Ihrem benutzerdefinierten Keine-Mail-Bild ein.\n\nBeispiel: images/custom_nomail.gif",
|
||||
"title": "Post und Pakete (Schritt 3 von 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon-Domain",
|
||||
"amazon_fwds": "Amazon hat E-Mail-Adressen weitergeleitet",
|
||||
"amazon_days": "Tage zurück, um nach Amazon-E-Mails zu suchen"
|
||||
},
|
||||
"description": "Bitte geben Sie die Domain ein, von der Amazon E-Mails sendet (z.B.: amazon.com oder amazon.de)\n\nWenn Sie weitergeleitete E-Mails von Amazon verwenden, trennen Sie bitte jede Adresse mit einem Komma oder geben Sie (keine) ein, um diese Einstellung zu löschen.\n\nHinweis: Wenn Sie im vorherigen Schritt einen Weiterleitungs-Header konfiguriert haben, wird das Feld für die Weiterleitungsadresse nicht angezeigt — der Header übernimmt die Zuordnung von Amazon-E-Mails automatisch.",
|
||||
"title": "Amazon Einstellungen"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Verzeichnis zum Speichern von Bildern"
|
||||
},
|
||||
"description": "Bitte geben Sie das Verzeichnis ein, in dem Ihre Bilder gespeichert werden sollen.\nStandardmäßig ist es automatisch ausgefüllt.",
|
||||
"title": "Speicherort für Bilder"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Verzeichnis zum Speichern von Bildern"
|
||||
},
|
||||
"description": "Bitte geben Sie das Verzeichnis ein, in dem Ihre Bilder gespeichert werden sollen.\nStandardmäßig ist es automatisch ausgefüllt.",
|
||||
"title": "Speicherort für Bilder"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Weiterleitungs-Header (z.B. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Weitergeleitete E-Mail-Adressen"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Wenn Ihr Weiterleitungsdienst den ursprünglichen Absender in einem Header enthält, geben Sie hier den Header-Namen ein. Dies hat Vorrang vor der Adressliste unten. Geben Sie '(none)' ein oder lassen Sie das Feld leer, um stattdessen die Adressliste zu verwenden.",
|
||||
"forwarded_emails": "Kommagetrennte Liste der Weiterleitungsadressen, von denen Nachrichten eingehen. Lassen Sie '(none)' stehen, wenn Sie den Weiterleitungs-Header oben verwenden."
|
||||
},
|
||||
"description": "Wählen Sie, wie weitergeleitete E-Mails abgeglichen werden sollen.\n\n**Option 1 — Weiterleitungs-Header**: Wenn Ihr Dienst (z.B. SimpleLogin) den ursprünglichen Absender in einem benutzerdefinierten Header enthält, geben Sie diesen Header-Namen ein. Mail and Packages verwendet ihn, um Zustell-Adressen automatisch abzugleichen.\n\n**Option 2 — Adressliste**: Geben Sie die Weiterleitungsadressen ein, die Ihr Dienst jedem Zusteller zugewiesen hat, getrennt durch Kommas.",
|
||||
"title": "Einstellungen für weitergeleitete E-Mails"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Weiterleitungs-Header (z.B. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Weitergeleitete E-Mail-Adressen"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Wenn Ihr Weiterleitungsdienst den ursprünglichen Absender in einem Header enthält, geben Sie hier den Header-Namen ein. Dies hat Vorrang vor der Adressliste unten. Geben Sie '(none)' ein oder lassen Sie das Feld leer, um stattdessen die Adressliste zu verwenden.",
|
||||
"forwarded_emails": "Kommagetrennte Liste der Weiterleitungsadressen, von denen Nachrichten eingehen. Lassen Sie '(none)' stehen, wenn Sie den Weiterleitungs-Header oben verwenden."
|
||||
},
|
||||
"description": "Wählen Sie, wie weitergeleitete E-Mails abgeglichen werden sollen.\n\n**Option 1 — Weiterleitungs-Header**: Wenn Ihr Dienst (z.B. SimpleLogin) den ursprünglichen Absender in einem benutzerdefinierten Header enthält, geben Sie diesen Header-Namen ein. Mail and Packages verwendet ihn, um Zustell-Adressen automatisch abzugleichen.\n\n**Option 2 — Adressliste**: Geben Sie die Weiterleitungsadressen ein, die Ihr Dienst jedem Zusteller zugewiesen hat, getrennt durch Kommas.",
|
||||
"title": "Einstellungen für weitergeleitete E-Mails"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Bitte geben Sie die Verbindungsinformationen Ihres Mail-Servers ein.",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Passwort",
|
||||
"port": "Port",
|
||||
"username": "Nutzername",
|
||||
"verify_ssl": "SSL-Zertifikat überprüfen",
|
||||
"imap_security": "IMAP-Sicherheit"
|
||||
},
|
||||
"title": "Briefe und Pakete (Schritt 1 von 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Passwort",
|
||||
"port": "Port",
|
||||
"username": "Nutzername",
|
||||
"imap_security": "IMAP-Sicherheit",
|
||||
"verify_ssl": "SSL-Zertifikat überprüfen"
|
||||
},
|
||||
"description": "Bitte geben Sie die Verbindungsinformationen Ihres Mail-Servers ein.",
|
||||
"title": "Briefe und Pakete (Schritt 1 von 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of Mail and Packages is allowed.",
|
||||
"reconfigure_successful": "Reconfigure Successful"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect or login to the mail server. Please check the log for details.",
|
||||
"invalid_path": "Please store the images in another directory.",
|
||||
"ffmpeg_not_found": "Generate MP4 requires ffmpeg",
|
||||
"amazon_domain": "Invalid forwarding email address.",
|
||||
"file_not_found": "Image file not found",
|
||||
"invalid_email_format": "Invalid email address format.",
|
||||
"missing_forwarded_emails": "Missing forwarded email addresses. You may enter '(none)' to clear this setting.",
|
||||
"path_not_found": "Directory not found",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Please enter the connection information of your mail server.",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"verify_ssl": "Verify SSL Cert",
|
||||
"imap_security": "IMAP Security"
|
||||
},
|
||||
"title": "Mail and Packages (Step 1 of 2)"
|
||||
},
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Mail Folder",
|
||||
"resources": "Sensors List",
|
||||
"scan_interval": "Scanning Interval (minutes, minimum 5)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)",
|
||||
"image_path": "Image Path",
|
||||
"gif_duration": "Image Duration (seconds)",
|
||||
"image_security": "Random Image Filename",
|
||||
"imap_timeout": "Mailbox scan time limit (seconds, minimum 10)",
|
||||
"generate_mp4": "Create mp4 from images",
|
||||
"allow_external": "Create image for notification apps",
|
||||
"custom_img": "Use custom USPS 'no mail' image?",
|
||||
"amazon_custom_img": "Use custom 'no Amazon delivery' image?",
|
||||
"ups_custom_img": "Use custom 'no UPS delivery' image?",
|
||||
"walmart_custom_img": "Use custom 'no Walmart delivery' image?",
|
||||
"fedex_custom_img": "Use custom 'no FedEx delivery' image?",
|
||||
"generic_custom_img": "Use custom 'no Generic delivery' image?",
|
||||
"post_de_custom_img": "Use custom Post DE 'no mail' image?",
|
||||
"generate_grid": "Create image grid for LLM vision models",
|
||||
"allow_forwarded_emails": "Allow forwarded emails in addition to a service's default (e.g. no-reply@usps.com)"
|
||||
},
|
||||
"data_description": {
|
||||
"imap_timeout": "Covers the entire scan — login plus every per-carrier IMAP search — not just connecting. Raise this if large mailboxes time out; lower it to fail faster."
|
||||
},
|
||||
"description": "Finish the configuration by customizing the following based on your email structure and Home Assistant installation.\n\nIf using Amazon forwarded emails please separate each address with a comma.",
|
||||
"title": "Mail and Packages (Step 2 of 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Path to custom image: (ie: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)",
|
||||
"post_de_custom_img_file": "Path to custom Post DE image: (ie: images/my_custom_no_post_de.gif)"
|
||||
},
|
||||
"description": "Enter the path and file name to your custom no mail image below.\n\nExample: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Step 3 of 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon forwarded email addresses",
|
||||
"amazon_days": "Days back to check for Amazon emails"
|
||||
},
|
||||
"description": "Please enter the domain Amazon sends email's from (ie: amazon.com or amazon.de)\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.\n\nNote: if you configured a forwarding header in the previous step, the forwarding address field will not appear — the header handles Amazon email matching automatically.",
|
||||
"title": "Amazon Settings"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"imap_security": "IMAP Security",
|
||||
"verify_ssl": "Verify SSL Cert"
|
||||
},
|
||||
"description": "Please enter the connection information of your mail server.",
|
||||
"title": "Mail and Packages (Step 1 of 2)"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Mail Folder",
|
||||
"scan_interval": "Scanning Interval (minutes, minimum 5)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)",
|
||||
"image_path": "Image Path",
|
||||
"gif_duration": "Image Duration (seconds)",
|
||||
"image_security": "Random Image Filename",
|
||||
"generate_mp4": "Create mp4 from images",
|
||||
"resources": "Sensors List",
|
||||
"imap_timeout": "Mailbox scan time limit (seconds, minimum 10)",
|
||||
"allow_external": "Create image for notification apps",
|
||||
"custom_img": "Use custom USPS 'no mail' image?",
|
||||
"amazon_custom_img": "Use custom 'no Amazon delivery' image?",
|
||||
"ups_custom_img": "Use custom 'no UPS delivery' image?",
|
||||
"walmart_custom_img": "Use custom 'no Walmart delivery' image?",
|
||||
"fedex_custom_img": "Use custom 'no FedEx delivery' image?",
|
||||
"generic_custom_img": "Use custom 'no Generic delivery' image?",
|
||||
"post_de_custom_img": "Use custom Post DE 'no mail' image?",
|
||||
"generate_grid": "Create image grid for LLM vision models",
|
||||
"allow_forwarded_emails": "Allow forwarded emails in addition to a service's default (e.g. no-reply@usps.com)"
|
||||
},
|
||||
"data_description": {
|
||||
"imap_timeout": "Covers the entire scan — login plus every per-carrier IMAP search — not just connecting. Raise this if large mailboxes time out; lower it to fail faster."
|
||||
},
|
||||
"description": "Finish the configuration by customizing the following based on your email structure and Home Assistant installation.\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.",
|
||||
"title": "Mail and Packages (Step 2 of 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Path to custom image: (i.e.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)",
|
||||
"post_de_custom_img_file": "Path to custom Post DE image: (i.e.: images/my_custom_no_post_de.gif)"
|
||||
},
|
||||
"description": "Enter the path and file name to your custom no mail image below.\n\nExample: images/custom_nomail.gif",
|
||||
"title": "Mail and Packages (Step 3 of 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon forwarded email addresses",
|
||||
"amazon_days": "Days back to check for Amazon emails"
|
||||
},
|
||||
"description": "Please enter the domain Amazon sends email's from (ie: amazon.com or amazon.de)\n\nIf using Amazon forwarded emails please separate each address with a comma or enter (none) to clear this setting.\n\nNote: if you configured a forwarding header in the previous step, the forwarding address field will not appear — the header handles Amazon email matching automatically.",
|
||||
"title": "Amazon Settings"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Directory to store images"
|
||||
},
|
||||
"description": "Please enter the directory you'd like your images to be stored in.\nThe default is auto populated.",
|
||||
"title": "Image storage location"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Directory to store images"
|
||||
},
|
||||
"description": "Please enter the directory you'd like your images to be stored in.\nThe default is auto populated.",
|
||||
"title": "Image storage location"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Forwarding Header (e.g. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Forwarded Email Addresses"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "If your forwarding service includes the original sender in a header, enter the header name here. This takes precedence over the address list below. Enter '(none)' or leave blank to use the address list instead.",
|
||||
"forwarded_emails": "Comma-separated list of forwarded addresses messages will arrive from. Leave as '(none)' if using the forwarding header above."
|
||||
},
|
||||
"description": "Choose how to match forwarded emails.\n\n**Option 1 — Forwarding header**: If your service (e.g. SimpleLogin) includes the original sender in a custom header, enter that header name. Mail and Packages will use it to match carrier addresses automatically.\n\n**Option 2 — Address list**: Enter the forwarding addresses your service assigned to each carrier, separated by commas.",
|
||||
"title": "Forwarded Email Settings"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Forwarding Header (e.g. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Forwarded Email Addresses"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "If your forwarding service includes the original sender in a header, enter the header name here. This takes precedence over the address list below. Enter '(none)' or leave blank to use the address list instead.",
|
||||
"forwarded_emails": "Comma-separated list of forwarded addresses messages will arrive from. Leave as '(none)' if using the forwarding header above."
|
||||
},
|
||||
"description": "Choose how to match forwarded emails.\n\n**Option 1 — Forwarding header**: If your service (e.g. SimpleLogin) includes the original sender in a custom header, enter that header name. Mail and Packages will use it to match carrier addresses automatically.\n\n**Option 2 — Address list**: Enter the forwarding addresses your service assigned to each carrier, separated by commas.",
|
||||
"title": "Forwarded Email Settings"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Solo se permite una única configuración de correo y paquetes.",
|
||||
"reconfigure_successful": "Reconfiguración Exitosa"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se puede conectar o iniciar sesión en el servidor de correo. Por favor, consulte el registro para más detalles.",
|
||||
"invalid_path": "Guarde las imágenes en otro directorio.",
|
||||
"ffmpeg_not_found": "Generar MP4 requiere ffmpeg",
|
||||
"amazon_domain": "Dirección de correo electrónico de reenvío no válida.",
|
||||
"file_not_found": "Archivo de imagen no encontrado",
|
||||
"invalid_email_format": "Formato de dirección de correo electrónico no válido.",
|
||||
"missing_forwarded_emails": "Faltan direcciones de correo electrónico reenviadas. Puede introducir '(none)' para borrar esta configuración.",
|
||||
"path_not_found": "Directorio no encontrado",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Carpeta de correo",
|
||||
"resources": "Lista de Sensores",
|
||||
"scan_interval": "Intervalo de escaneo (minutos)",
|
||||
"image_path": "Ruta de la imagen",
|
||||
"gif_duration": "Duración de la imagen (segundos)",
|
||||
"image_security": "Nombre de archivo de imagen aleatoria",
|
||||
"imap_timeout": "Tiempo en segundos antes del tiempo de espera de la conexión (segundos, mínimo 10)",
|
||||
"generate_mp4": "Crea mp4 a partir de imágenes",
|
||||
"allow_external": "Crea imagen para aplicaciones de notificación",
|
||||
"custom_img": "¿Usar imagen personalizada de USPS 'sin correo'?",
|
||||
"amazon_custom_img": "¿Usar imagen personalizada de Amazon 'sin entrega'?",
|
||||
"ups_custom_img": "¿Usar imagen personalizada de UPS 'sin entrega'?",
|
||||
"walmart_custom_img": "¿Usar imagen personalizada de Walmart 'sin entrega'?",
|
||||
"fedex_custom_img": "¿Usar imagen personalizada de FedEx 'sin entrega'?",
|
||||
"generic_custom_img": "¿Usar imagen personalizada genérica 'sin entrega'?",
|
||||
"generate_grid": "Crear cuadrícula de imágenes para modelos de visión LLM",
|
||||
"allow_forwarded_emails": "Permitir correos electrónicos reenviados además del valor predeterminado de un servicio (p. ej., no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Termine la configuración personalizando lo siguiente según su estructura de correo electrónico y la instalación de Home Assistant. \n\n Para obtener detalles sobre las opciones [Integración de correo y paquetes] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) revise las [configuración, plantillas , y la sección de automatizaciones] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) en GitHub.",
|
||||
"title": "Correo y paquetes (Paso 2 de 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Ruta a la imagen personalizada: (es decir: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Ingrese la ruta y el nombre del archivo de su imagen personalizada de no correo a continuación.\n\nEjemplo: images/custom_nomail.gif",
|
||||
"title": "Correo y Paquetes (Paso 3 de 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Dominio de Amazon",
|
||||
"amazon_fwds": "Amazon reenvió direcciones de correo electrónico",
|
||||
"amazon_days": "Días atrás para verificar los correos electrónicos de Amazon"
|
||||
},
|
||||
"description": "Por favor, introduzca el dominio desde el cual Amazon envía correos electrónicos (es decir: amazon.com o amazon.de)\n\nSi utiliza correos electrónicos reenviados por Amazon, separe cada dirección con una coma o introduzca (ninguno) para borrar esta configuración.\n\nNota: si configuró un encabezado de reenvío en el paso anterior, el campo de dirección de reenvío no aparecerá — el encabezado gestiona la coincidencia de correos electrónicos de Amazon automáticamente.",
|
||||
"title": "Configuración de Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Carpeta de correo",
|
||||
"scan_interval": "Intervalo de escaneo (minutos)",
|
||||
"image_path": "Ruta de la imagen",
|
||||
"gif_duration": "Duración de la imagen (segundos)",
|
||||
"image_security": "Nombre de archivo de imagen aleatoria",
|
||||
"generate_mp4": "Crea mp4 a partir de imágenes",
|
||||
"resources": "Lista de Sensores",
|
||||
"imap_timeout": "Tiempo en segundos antes del tiempo de espera de la conexión (segundos, mínimo 10)",
|
||||
"allow_external": "Crea imagen para aplicaciones de notificación",
|
||||
"custom_img": "¿Usar imagen personalizada de USPS 'sin correo'?",
|
||||
"amazon_custom_img": "¿Usar imagen personalizada de Amazon 'sin entrega'?",
|
||||
"ups_custom_img": "¿Usar imagen personalizada de UPS 'sin entrega'?",
|
||||
"walmart_custom_img": "¿Usar imagen personalizada de Walmart 'sin entrega'?",
|
||||
"fedex_custom_img": "¿Usar imagen personalizada de FedEx 'sin entrega'?",
|
||||
"generic_custom_img": "¿Usar imagen personalizada genérica 'sin entrega'?",
|
||||
"generate_grid": "Crear cuadrícula de imágenes para modelos de visión LLM",
|
||||
"allow_forwarded_emails": "Permitir correos electrónicos reenviados además del valor predeterminado de un servicio (p. ej., no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Finalice la configuración personalizando lo siguiente en función de la estructura de su correo electrónico y la instalación de Home Assistant.\n\nPara obtener detalles sobre las opciones de [integración de correo y paquetes](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), revise la [sección de configuración, plantillas y automatizaciones](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) en GitHub.\n\nSi utiliza correos electrónicos reenviados de Amazon, separe cada dirección con una coma o ingrese (ninguno) para borrar esta configuración.",
|
||||
"title": "Correo y paquetes (Paso 2 de 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Ruta a la imagen personalizada: (es decir: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Ingrese la ruta y el nombre del archivo de su imagen personalizada de no correo a continuación.\n\nEjemplo: images/custom_nomail.gif",
|
||||
"title": "Correo y Paquetes (Paso 3 de 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Dominio de Amazon",
|
||||
"amazon_fwds": "Amazon reenvió direcciones de correo electrónico",
|
||||
"amazon_days": "Días atrás para verificar los correos electrónicos de Amazon"
|
||||
},
|
||||
"description": "Por favor, introduzca el dominio desde el cual Amazon envía correos electrónicos (es decir: amazon.com o amazon.de)\n\nSi utiliza correos electrónicos reenviados por Amazon, separe cada dirección con una coma o introduzca (ninguno) para borrar esta configuración.\n\nNota: si configuró un encabezado de reenvío en el paso anterior, el campo de dirección de reenvío no aparecerá — el encabezado gestiona la coincidencia de correos electrónicos de Amazon automáticamente.",
|
||||
"title": "Configuración de Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Directorio para almacenar imágenes"
|
||||
},
|
||||
"description": "Por favor, introduzca el directorio en el que le gustaría almacenar sus imágenes. \nEl valor predeterminado se rellena automáticamente.",
|
||||
"title": "Ubicación de almacenamiento de imágenes"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Directorio para almacenar imágenes"
|
||||
},
|
||||
"description": "Por favor, introduzca el directorio en el que le gustaría almacenar sus imágenes. \nEl valor predeterminado se rellena automáticamente.",
|
||||
"title": "Ubicación de almacenamiento de imágenes"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabecera de reenvío (p. ej. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Direcciones de correo electrónico reenviadas"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si su servicio de reenvío incluye el remitente original en una cabecera, ingrese el nombre de la cabecera aquí. Esto tiene prioridad sobre la lista de direcciones a continuación. Ingrese '(none)' o deje en blanco para usar la lista de direcciones en su lugar.",
|
||||
"forwarded_emails": "Lista separada por comas de las direcciones de reenvío de las que llegarán los mensajes. Deje '(none)' si usa la cabecera de reenvío de arriba."
|
||||
},
|
||||
"description": "Elija cómo hacer coincidir los correos electrónicos reenviados.\n\n**Opción 1 — Cabecera de reenvío**: Si su servicio (p. ej. SimpleLogin) incluye el remitente original en una cabecera personalizada, ingrese ese nombre de cabecera. Mail and Packages lo usará para hacer coincidir las direcciones de los transportistas automáticamente.\n\n**Opción 2 — Lista de direcciones**: Ingrese las direcciones de reenvío que su servicio asignó a cada transportista, separadas por comas.",
|
||||
"title": "Configuración de correos electrónicos reenviados"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabecera de reenvío (p. ej. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Direcciones de correo electrónico reenviadas"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si su servicio de reenvío incluye el remitente original en una cabecera, ingrese el nombre de la cabecera aquí. Esto tiene prioridad sobre la lista de direcciones a continuación. Ingrese '(none)' o deje en blanco para usar la lista de direcciones en su lugar.",
|
||||
"forwarded_emails": "Lista separada por comas de las direcciones de reenvío de las que llegarán los mensajes. Deje '(none)' si usa la cabecera de reenvío de arriba."
|
||||
},
|
||||
"description": "Elija cómo hacer coincidir los correos electrónicos reenviados.\n\n**Opción 1 — Cabecera de reenvío**: Si su servicio (p. ej. SimpleLogin) incluye el remitente original en una cabecera personalizada, ingrese ese nombre de cabecera. Mail and Packages lo usará para hacer coincidir las direcciones de los transportistas automáticamente.\n\n**Opción 2 — Lista de direcciones**: Ingrese las direcciones de reenvío que su servicio asignó a cada transportista, separadas por comas.",
|
||||
"title": "Configuración de correos electrónicos reenviados"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Por favor, ingrese la información de conexión de su servidor de correo.",
|
||||
"data": {
|
||||
"host": "Anfitrión",
|
||||
"password": "Contraseña",
|
||||
"port": "Puerto",
|
||||
"username": "Nombre de usuario",
|
||||
"verify_ssl": "Verificar Certificado SSL",
|
||||
"imap_security": "Seguridad IMAP"
|
||||
},
|
||||
"title": "Correo y paquetes (Paso 1 de 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Anfitrión",
|
||||
"password": "Contraseña",
|
||||
"port": "Puerto",
|
||||
"username": "Nombre de usuario",
|
||||
"imap_security": "Seguridad IMAP",
|
||||
"verify_ssl": "Verificar Certificado SSL"
|
||||
},
|
||||
"description": "Por favor, ingrese la información de conexión de su servidor de correo.",
|
||||
"title": "Correo y paquetes (Paso 1 de 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Solo se permite una única configuración de correo y paquetes.",
|
||||
"reconfigure_successful": "Reconfiguración Exitosa"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se puede conectar o iniciar sesión en el servidor de correo. Por favor, consulte el registro para más detalles.",
|
||||
"invalid_path": "Guarde las imágenes en otro directorio.",
|
||||
"ffmpeg_not_found": "Generar MP4 requiere ffmpeg",
|
||||
"amazon_domain": "Dirección de correo electrónico de reenvío no válida.",
|
||||
"file_not_found": "Archivo de imagen no encontrado",
|
||||
"invalid_email_format": "Formato de dirección de correo electrónico inválido.",
|
||||
"missing_forwarded_emails": "Faltan direcciones de correo electrónico reenviadas. Puede introducir '(none)' para borrar esta configuración.",
|
||||
"path_not_found": "Directorio no encontrado",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Carpeta de correo",
|
||||
"resources": "Lista de Sensores",
|
||||
"scan_interval": "Intervalo de escaneo (minutos)",
|
||||
"image_path": "Ruta de la imagen",
|
||||
"gif_duration": "Duración de la imagen (segundos)",
|
||||
"image_security": "Nombre de archivo de imagen aleatoria",
|
||||
"imap_timeout": "Tiempo en segundos antes del tiempo de espera de la conexión (segundos, mínimo 10)",
|
||||
"generate_mp4": "Crea mp4 a partir de imágenes",
|
||||
"allow_external": "Crea imagen para aplicaciones de notificación",
|
||||
"custom_img": "¿Usar imagen personalizada de USPS 'sin correo'?",
|
||||
"amazon_custom_img": "¿Usar imagen personalizada de Amazon 'sin entrega'?",
|
||||
"ups_custom_img": "¿Usar imagen personalizada de UPS 'sin entrega'?",
|
||||
"walmart_custom_img": "¿Usar imagen personalizada de Walmart 'sin entrega'?",
|
||||
"fedex_custom_img": "¿Usar imagen personalizada de FedEx 'sin entrega'?",
|
||||
"generic_custom_img": "¿Usar imagen personalizada genérica 'sin entrega'?",
|
||||
"generate_grid": "Crear cuadrícula de imágenes para modelos de visión LLM",
|
||||
"allow_forwarded_emails": "Permitir correos electrónicos reenviados además del valor predeterminado de un servicio (p. ej., no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Termine la configuración personalizando lo siguiente según su estructura de correo electrónico y la instalación de Home Assistant. \n\n Para obtener detalles sobre las opciones [Integración de correo y paquetes] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) revise las [configuración, plantillas , y la sección de automatizaciones] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) en GitHub.",
|
||||
"title": "Correo y paquetes (Paso 2 de 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Ruta a la imagen personalizada: (es decir: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Ingrese la ruta y el nombre del archivo de su imagen personalizada de no correo a continuación.\n\nEjemplo: images/custom_nomail.gif",
|
||||
"title": "Correo y paquetes (Paso 3 de 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Dominio de Amazon",
|
||||
"amazon_fwds": "Amazon reenvió direcciones de correo electrónico",
|
||||
"amazon_days": "Días atrás para verificar los correos electrónicos de Amazon"
|
||||
},
|
||||
"description": "Por favor, ingrese el dominio desde el cual Amazon envía correos electrónicos (es decir: amazon.com o amazon.de)\n\nSi utiliza correos electrónicos reenviados por Amazon, separe cada dirección con una coma o ingrese (ninguno) para borrar esta configuración.\n\nNota: si configuró un encabezado de reenvío en el paso anterior, el campo de dirección de reenvío no aparecerá — el encabezado gestiona la coincidencia de correos electrónicos de Amazon automáticamente.",
|
||||
"title": "Configuración de Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Carpeta de correo",
|
||||
"scan_interval": "Intervalo de escaneo (minutos)",
|
||||
"image_path": "Ruta de la imagen",
|
||||
"gif_duration": "Duración de la imagen (segundos)",
|
||||
"image_security": "Nombre de archivo de imagen aleatoria",
|
||||
"generate_mp4": "Crea mp4 a partir de imágenes",
|
||||
"resources": "Lista de Sensores",
|
||||
"imap_timeout": "Tiempo en segundos antes del tiempo de espera de la conexión (segundos, mínimo 10)",
|
||||
"allow_external": "Crea imagen para aplicaciones de notificación",
|
||||
"custom_img": "¿Usar imagen personalizada de USPS 'sin correo'?",
|
||||
"amazon_custom_img": "¿Usar imagen personalizada de Amazon 'sin entrega'?",
|
||||
"ups_custom_img": "¿Usar imagen personalizada de UPS 'sin entrega'?",
|
||||
"walmart_custom_img": "¿Usar imagen personalizada de Walmart 'sin entrega'?",
|
||||
"fedex_custom_img": "¿Usar imagen personalizada de FedEx 'sin entrega'?",
|
||||
"generic_custom_img": "¿Usar imagen personalizada genérica 'sin entrega'?",
|
||||
"generate_grid": "Crear cuadrícula de imágenes para modelos de visión LLM",
|
||||
"allow_forwarded_emails": "Permitir correos electrónicos reenviados además del valor predeterminado de un servicio (p. ej., no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Finalice la configuración personalizando lo siguiente en función de la estructura de su correo electrónico y la instalación de Home Assistant.\n\nPara obtener detalles sobre las opciones de [integración de correo y paquetes](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), revise la [sección de configuración, plantillas y automatizaciones](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) en GitHub.\n\nSi utiliza correos electrónicos reenviados de Amazon, separe cada dirección con una coma o ingrese (ninguno) para borrar esta configuración.",
|
||||
"title": "Correo y paquetes (Paso 2 de 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Ruta a la imagen personalizada: (es decir: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Ingrese la ruta y el nombre del archivo de su imagen personalizada de no correo a continuación.\n\nEjemplo: images/custom_nomail.gif",
|
||||
"title": "Correo y paquetes (Paso 3 de 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Dominio de Amazon",
|
||||
"amazon_fwds": "Amazon reenvió direcciones de correo electrónico",
|
||||
"amazon_days": "Días atrás para verificar los correos electrónicos de Amazon"
|
||||
},
|
||||
"description": "Por favor, ingrese el dominio desde el cual Amazon envía correos electrónicos (es decir: amazon.com o amazon.de)\n\nSi utiliza correos electrónicos reenviados por Amazon, separe cada dirección con una coma o ingrese (ninguno) para borrar esta configuración.\n\nNota: si configuró un encabezado de reenvío en el paso anterior, el campo de dirección de reenvío no aparecerá — el encabezado gestiona la coincidencia de correos electrónicos de Amazon automáticamente.",
|
||||
"title": "Configuración de Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Directorio para almacenar imágenes"
|
||||
},
|
||||
"description": "Por favor, ingrese el directorio en el que le gustaría almacenar sus imágenes.\nEl predeterminado se llena automáticamente.",
|
||||
"title": "Ubicación de almacenamiento de imágenes"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Directorio para almacenar imágenes"
|
||||
},
|
||||
"description": "Por favor, ingrese el directorio en el que le gustaría almacenar sus imágenes.\nEl predeterminado se llena automáticamente.",
|
||||
"title": "Ubicación de almacenamiento de imágenes"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabecera de reenvío (p. ej. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Direcciones de correo electrónico reenviadas"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si su servicio de reenvío incluye el remitente original en una cabecera, ingrese el nombre de la cabecera aquí. Esto tiene prioridad sobre la lista de direcciones a continuación. Ingrese '(none)' o deje en blanco para usar la lista de direcciones en su lugar.",
|
||||
"forwarded_emails": "Lista separada por comas de las direcciones de reenvío de las que llegarán los mensajes. Deje '(none)' si usa la cabecera de reenvío de arriba."
|
||||
},
|
||||
"description": "Elija cómo hacer coincidir los correos electrónicos reenviados.\n\n**Opción 1 — Cabecera de reenvío**: Si su servicio (p. ej. SimpleLogin) incluye el remitente original en una cabecera personalizada, ingrese ese nombre de cabecera. Mail and Packages lo usará para hacer coincidir las direcciones de los transportistas automáticamente.\n\n**Opción 2 — Lista de direcciones**: Ingrese las direcciones de reenvío que su servicio asignó a cada transportista, separadas por comas.",
|
||||
"title": "Configuración de correos electrónicos reenviados"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabecera de reenvío (p. ej. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Direcciones de correo electrónico reenviadas"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si su servicio de reenvío incluye el remitente original en una cabecera, ingrese el nombre de la cabecera aquí. Esto tiene prioridad sobre la lista de direcciones a continuación. Ingrese '(none)' o deje en blanco para usar la lista de direcciones en su lugar.",
|
||||
"forwarded_emails": "Lista separada por comas de las direcciones de reenvío de las que llegarán los mensajes. Deje '(none)' si usa la cabecera de reenvío de arriba."
|
||||
},
|
||||
"description": "Elija cómo hacer coincidir los correos electrónicos reenviados.\n\n**Opción 1 — Cabecera de reenvío**: Si su servicio (p. ej. SimpleLogin) incluye el remitente original en una cabecera personalizada, ingrese ese nombre de cabecera. Mail and Packages lo usará para hacer coincidir las direcciones de los transportistas automáticamente.\n\n**Opción 2 — Lista de direcciones**: Ingrese las direcciones de reenvío que su servicio asignó a cada transportista, separadas por comas.",
|
||||
"title": "Configuración de correos electrónicos reenviados"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Por favor, ingrese la información de conexión de su servidor de correo.",
|
||||
"data": {
|
||||
"host": "Anfitrión",
|
||||
"password": "Contraseña",
|
||||
"port": "Puerto",
|
||||
"username": "Nombre de usuario",
|
||||
"verify_ssl": "Verificar Certificado SSL",
|
||||
"imap_security": "Seguridad IMAP"
|
||||
},
|
||||
"title": "Correo y paquetes (Paso 1 de 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Anfitrión",
|
||||
"password": "Contraseña",
|
||||
"port": "Puerto",
|
||||
"username": "Nombre de usuario",
|
||||
"imap_security": "Seguridad IMAP",
|
||||
"verify_ssl": "Verificar Certificado SSL"
|
||||
},
|
||||
"description": "Por favor, ingrese la información de conexión de su servidor de correo.",
|
||||
"title": "Correo y paquetes (Paso 1 de 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Vain yksi posti- ja pakettien kokoonpano on sallittu.",
|
||||
"reconfigure_successful": "Uudelleenmääritys onnistui"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Ei voida muodostaa yhteyttä tai kirjautua sisään postipalvelimeen. Tarkista lokista yksityiskohdat.",
|
||||
"invalid_path": "Tallenna kuvat toiseen hakemistoon.",
|
||||
"ffmpeg_not_found": "MP4:n luominen vaatii ffmpeg:n",
|
||||
"amazon_domain": "Virheellinen edelleenlähetettävä sähköpostiosoite.",
|
||||
"file_not_found": "Kuvatiedostoa ei löydy",
|
||||
"invalid_email_format": "Virheellinen sähköpostiosoitteen muoto.",
|
||||
"missing_forwarded_emails": "Puuttuvat edelleenlähetetyt sähköpostiosoitteet. Voit syöttää '(none)' tyhjentääksesi tämän asetuksen.",
|
||||
"path_not_found": "Hakemistoa ei löydy",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Mail-kansio",
|
||||
"resources": "Anturiluettelo",
|
||||
"scan_interval": "Skannausväli (minuutit)",
|
||||
"image_path": "Kuvan polku",
|
||||
"gif_duration": "Kuvan kesto (sekunteina)",
|
||||
"image_security": "Satunnainen kuvatiedostonimi",
|
||||
"imap_timeout": "Aika sekunneissa ennen yhteyden aikakatkaisua (sekuntia, vähintään 10)",
|
||||
"generate_mp4": "Luo mp4 kuvista",
|
||||
"allow_external": "Luo kuva ilmoitussovelluksille",
|
||||
"custom_img": "Käytä mukautettua USPS 'ei postia' kuvaa?",
|
||||
"amazon_custom_img": "Käytä mukautettua Amazon 'ei toimituksia' kuvaa?",
|
||||
"ups_custom_img": "Käytä mukautettua UPS 'ei toimituksia' kuvaa?",
|
||||
"walmart_custom_img": "Käytä mukautettua Walmart 'ei toimituksia' kuvaa?",
|
||||
"fedex_custom_img": "Käytä mukautettua FedEx 'ei toimituksia' kuvaa?",
|
||||
"generic_custom_img": "Käytä mukautettua yleistä 'ei toimituksia' kuvaa?",
|
||||
"generate_grid": "Luo kuvaverkko LLM-näkömalleille",
|
||||
"allow_forwarded_emails": "Salli edelleenlähetetyt sähköpostit palvelun oletusarvon lisäksi (esim. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Viimeistele kokoonpano mukauttamalla seuraava sähköpostirakenteen ja Home Assistant -asennuksen perusteella. \n\n Lisätietoja [Posti ja paketit-integroinnista] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) -asetuksista on [kokoonpano, mallit , ja automaatiot-osio] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) GitHubissa.",
|
||||
"title": "Posti ja paketit (vaihe 2/2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Polku mukautettuun kuvaan: (esim: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Syötä alla olevaan kenttään polku ja tiedostonimi mukautetulle ei-postia -kuvallesi.\n\nEsimerkki: kuvat/mukautettu_eipostia.gif",
|
||||
"title": "Posti ja paketit (vaihe 3/3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazonin verkkotunnus",
|
||||
"amazon_fwds": "Amazon välitti sähköpostiosoitteet",
|
||||
"amazon_days": "Päiviä, joiden aikana tarkistetaan Amazonin sähköpostit"
|
||||
},
|
||||
"description": "Anna domaini, josta Amazon lähettää sähköposteja (esim: amazon.com tai amazon.de)\n\nJos käytät Amazonin välittämiä sähköposteja, erottele jokainen osoite pilkulla tai kirjoita (none) tyhjentääksesi tämän asetuksen.\n\nHuomio: jos määritit edelleenlähetyksen otsikon edellisessä vaiheessa, edelleenlähetysosoite-kenttä ei näy — otsikko käsittelee Amazon-sähköpostien täsmäytyksen automaattisesti.",
|
||||
"title": "Amazonin asetukset"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Mail-kansio",
|
||||
"scan_interval": "Skannausväli (minuutit)",
|
||||
"image_path": "Kuvan polku",
|
||||
"gif_duration": "Kuvan kesto (sekunteina)",
|
||||
"image_security": "Satunnainen kuvatiedostonimi",
|
||||
"generate_mp4": "Luo mp4 kuvista",
|
||||
"resources": "Anturiluettelo",
|
||||
"imap_timeout": "Aika sekunneissa ennen yhteyden aikakatkaisua (sekuntia, vähintään 10)",
|
||||
"allow_external": "Luo kuva ilmoitussovelluksille",
|
||||
"custom_img": "Käytä mukautettua USPS 'ei postia' kuvaa?",
|
||||
"amazon_custom_img": "Käytä mukautettua Amazon 'ei toimituksia' kuvaa?",
|
||||
"ups_custom_img": "Käytä mukautettua UPS 'ei toimituksia' kuvaa?",
|
||||
"walmart_custom_img": "Käytä mukautettua Walmart 'ei toimituksia' kuvaa?",
|
||||
"fedex_custom_img": "Käytä mukautettua FedEx 'ei toimituksia' kuvaa?",
|
||||
"generic_custom_img": "Käytä mukautettua yleistä 'ei toimituksia' kuvaa?",
|
||||
"generate_grid": "Luo kuvaverkko LLM-näkömalleille",
|
||||
"allow_forwarded_emails": "Salli edelleenlähetetyt sähköpostit palvelun oletusarvon lisäksi (esim. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Suorita määritys loppuun mukauttamalla seuraavat sähköpostirakenteeseesi ja Home Assistant -asennukseesi perustuen.\n\nLisätietoja [Mail and Packages -integraation](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) vaihtoehdoista löydät GitHubista [määritys-, mallit- ja automaatiot-osiossa](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration).\n\nJos käytät Amazonin välittämiä sähköposteja, erottele jokainen osoite pilkulla tai syötä (none) tyhjentääksesi tämän asetuksen.",
|
||||
"title": "Posti ja paketit (vaihe 2/2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Polku mukautettuun kuvaan: (esim.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Syötä alla olevaan kenttään polku ja tiedostonimi mukautetulle ei-postia -kuvallesi.\n\nEsimerkki: kuvat/mukautettu_eipostia.gif",
|
||||
"title": "Posti ja paketit (vaihe 3/3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazonin verkkotunnus",
|
||||
"amazon_fwds": "Amazon välitti sähköpostiosoitteet",
|
||||
"amazon_days": "Päiviä, joiden aikana tarkistetaan Amazonin sähköpostit"
|
||||
},
|
||||
"description": "Anna domaini, josta Amazon lähettää sähköposteja (esim: amazon.com tai amazon.de)\n\nJos käytät Amazonin välittämiä sähköposteja, erottele jokainen osoite pilkulla tai kirjoita (none) tyhjentääksesi tämän asetuksen.\n\nHuomio: jos määritit edelleenlähetyksen otsikon edellisessä vaiheessa, edelleenlähetysosoite-kenttä ei näy — otsikko käsittelee Amazon-sähköpostien täsmäytyksen automaattisesti.",
|
||||
"title": "Amazonin asetukset"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Hakemisto kuvien tallentamiseen"
|
||||
},
|
||||
"description": "Anna hakemisto, johon haluat kuviesi tallentuvan.\nOletusarvo täytetään automaattisesti.",
|
||||
"title": "Kuvien tallennuspaikka"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Hakemisto kuvien tallentamiseen"
|
||||
},
|
||||
"description": "Anna hakemisto, johon haluat kuviesi tallentuvan.\nOletusarvo täytetään automaattisesti.",
|
||||
"title": "Kuvien tallennuspaikka"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Välitysotsake (esim. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Edelleenlähetetyt sähköpostiosoitteet"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Jos välityspalvelusi sisältää alkuperäisen lähettäjän otsikossa, syötä otsikon nimi tähän. Tämä ohittaa alla olevan osoitelistan. Syötä '(none)' tai jätä tyhjäksi käyttääksesi osoitelistaa.",
|
||||
"forwarded_emails": "Pilkuilla eroteltu lista välitysosoitteista, joista viestit saapuvat. Jätä '(none)', jos käytät yllä olevaa välitysotsiketta."
|
||||
},
|
||||
"description": "Valitse, miten välitetyt sähköpostit yhdistetään.\n\n**Vaihtoehto 1 — Välitysotsake**: Jos palvelusi (esim. SimpleLogin) sisältää alkuperäisen lähettäjän mukautetussa otsakkeessa, syötä kyseinen otsakkeen nimi. Mail and Packages käyttää sitä kuriiriosoitteiden automaattiseen yhdistämiseen.\n\n**Vaihtoehto 2 — Osoitelista**: Syötä välitysosoitteet, jotka palvelusi on määrittänyt kullekin kuriirille, pilkuilla eroteltuna.",
|
||||
"title": "Välitettyjen sähköpostien asetukset"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Välitysotsake (esim. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Edelleenlähetetyt sähköpostiosoitteet"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Jos välityspalvelusi sisältää alkuperäisen lähettäjän otsikossa, syötä otsikon nimi tähän. Tämä ohittaa alla olevan osoitelistan. Syötä '(none)' tai jätä tyhjäksi käyttääksesi osoitelistaa.",
|
||||
"forwarded_emails": "Pilkuilla eroteltu lista välitysosoitteista, joista viestit saapuvat. Jätä '(none)', jos käytät yllä olevaa välitysotsiketta."
|
||||
},
|
||||
"description": "Valitse, miten välitetyt sähköpostit yhdistetään.\n\n**Vaihtoehto 1 — Välitysotsake**: Jos palvelusi (esim. SimpleLogin) sisältää alkuperäisen lähettäjän mukautetussa otsakkeessa, syötä kyseinen otsakkeen nimi. Mail and Packages käyttää sitä kuriiriosoitteiden automaattiseen yhdistämiseen.\n\n**Vaihtoehto 2 — Osoitelista**: Syötä välitysosoitteet, jotka palvelusi on määrittänyt kullekin kuriirille, pilkuilla eroteltuna.",
|
||||
"title": "Välitettyjen sähköpostien asetukset"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Anna postipalvelimesi yhteystiedot.",
|
||||
"data": {
|
||||
"host": "isäntä",
|
||||
"password": "Salasana",
|
||||
"port": "portti",
|
||||
"username": "Käyttäjätunnus",
|
||||
"verify_ssl": "Varmenna SSL-sertifikaatti",
|
||||
"imap_security": "IMAP-tietoturva"
|
||||
},
|
||||
"title": "Posti ja paketit (vaihe 1/2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "isäntä",
|
||||
"password": "Salasana",
|
||||
"port": "portti",
|
||||
"username": "Käyttäjätunnus",
|
||||
"imap_security": "IMAP-tietoturva",
|
||||
"verify_ssl": "Varmenna SSL-sertifikaatti"
|
||||
},
|
||||
"description": "Anna postipalvelimesi yhteystiedot.",
|
||||
"title": "Posti ja paketit (vaihe 1/2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Une seule configuration de Mail et Packages est autorisée.",
|
||||
"reconfigure_successful": "Reconfiguration réussie"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossible de se connecter ou de se connecter au serveur de messagerie. Veuillez consulter le journal pour plus de détails.",
|
||||
"invalid_path": "Veuillez stocker les images dans un autre répertoire.",
|
||||
"ffmpeg_not_found": "Générer MP4 nécessite ffmpeg",
|
||||
"amazon_domain": "Adresse e-mail de transfert invalide.",
|
||||
"file_not_found": "Fichier image non trouvé",
|
||||
"invalid_email_format": "Format d'adresse e-mail invalide.",
|
||||
"missing_forwarded_emails": "Adresses e-mail de transfert manquantes. Vous pouvez entrer '(none)' pour effacer ce paramètre.",
|
||||
"path_not_found": "Répertoire non trouvé",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Dossier de messagerie",
|
||||
"resources": "Liste des capteurs",
|
||||
"scan_interval": "Intervalle de numérisation (minutes)",
|
||||
"image_path": "Chemin de l'image",
|
||||
"gif_duration": "Durée de l'image (secondes)",
|
||||
"image_security": "Nom de fichier d'image aléatoire",
|
||||
"imap_timeout": "Temps en secondes avant la fin de la connexion (secondes, minimum 10)",
|
||||
"generate_mp4": "Créer mp4 à partir d'images",
|
||||
"allow_external": "Créer une image pour les applications de notification",
|
||||
"custom_img": "Utiliser une image personnalisée USPS 'pas de courrier' ?",
|
||||
"amazon_custom_img": "Utiliser une image personnalisée Amazon 'pas de livraison' ?",
|
||||
"ups_custom_img": "Utiliser une image personnalisée UPS 'pas de livraison' ?",
|
||||
"walmart_custom_img": "Utiliser une image personnalisée Walmart 'pas de livraison' ?",
|
||||
"fedex_custom_img": "Utiliser une image personnalisée FedEx 'pas de livraison' ?",
|
||||
"generic_custom_img": "Utiliser une image personnalisée générique 'pas de livraison' ?",
|
||||
"generate_grid": "Créer une grille d'images pour les modèles de vision LLM",
|
||||
"allow_forwarded_emails": "Autoriser les e-mails transférés en plus de la valeur par défaut d'un service (par ex. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Terminez la configuration en personnalisant les éléments suivants en fonction de votre structure de messagerie et de l'installation de Home Assistant. \n\n Pour plus de détails sur les [Intégration de messagerie et de packages] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), passez en revue les [configuration, modèles et section automatisations] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) sur GitHub.",
|
||||
"title": "Courrier et colis (étape 2 sur 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Chemin vers l'image personnalisée : (par exemple : images/mon_image_personnalisée_pas_de_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Entrez le chemin et le nom de fichier de votre image personnalisée de non-réception de courrier ci-dessous.\n\nExemple : images/custom_nomail.gif",
|
||||
"title": "Courrier et colis (Étape 3 sur 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domaine Amazon",
|
||||
"amazon_fwds": "Amazon a transmis des adresses e-mail",
|
||||
"amazon_days": "Jours en arrière pour vérifier les emails Amazon"
|
||||
},
|
||||
"description": "Veuillez entrer le domaine à partir duquel Amazon envoie des emails (par exemple : amazon.com ou amazon.de)\n\nSi vous utilisez des emails transférés par Amazon, veuillez séparer chaque adresse par une virgule ou entrer (aucun) pour effacer ce paramètre.\n\nRemarque : si vous avez configuré un en-tête de transfert dans l'étape précédente, le champ d'adresse de transfert n'apparaîtra pas — l'en-tête gère automatiquement la correspondance des e-mails Amazon.",
|
||||
"title": "Paramètres Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Dossier de messagerie",
|
||||
"scan_interval": "Intervalle de numérisation (minutes)",
|
||||
"image_path": "Chemin de l'image",
|
||||
"gif_duration": "Durée de l'image (secondes)",
|
||||
"image_security": "Nom de fichier d'image aléatoire",
|
||||
"generate_mp4": "Créer mp4 à partir d'images",
|
||||
"resources": "Liste des capteurs",
|
||||
"imap_timeout": "Temps en secondes avant la fin de la connexion (secondes, minimum 10)",
|
||||
"allow_external": "Créer une image pour les applications de notification",
|
||||
"custom_img": "Utiliser une image personnalisée USPS 'pas de courrier' ?",
|
||||
"amazon_custom_img": "Utiliser une image personnalisée Amazon 'pas de livraison' ?",
|
||||
"ups_custom_img": "Utiliser une image personnalisée UPS 'pas de livraison' ?",
|
||||
"walmart_custom_img": "Utiliser une image personnalisée Walmart 'pas de livraison' ?",
|
||||
"fedex_custom_img": "Utiliser une image personnalisée FedEx 'pas de livraison' ?",
|
||||
"generic_custom_img": "Utiliser une image personnalisée générique 'pas de livraison' ?",
|
||||
"generate_grid": "Créer une grille d'images pour les modèles de vision LLM",
|
||||
"allow_forwarded_emails": "Autoriser les e-mails transférés en plus de la valeur par défaut d'un service (par ex. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Terminez la configuration en personnalisant ce qui suit en fonction de la structure de votre courrier électronique et de l'installation de Home Assistant.\n\nPour plus de détails sur les options d'[intégration de courrier et de colis](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), consultez la [section configuration, modèles et automatisations](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) sur GitHub.\n\nSi vous utilisez des courriels transférés par Amazon, veuillez séparer chaque adresse par une virgule ou entrer (aucun) pour effacer ce paramètre.",
|
||||
"title": "Courrier et colis (étape 2 sur 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Chemin vers l'image personnalisée : (par exemple : images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Entrez le chemin et le nom de fichier de votre image personnalisée de non-réception de courrier ci-dessous.\n\nExemple : images/custom_nomail.gif",
|
||||
"title": "Courrier et colis (Étape 3 sur 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domaine Amazon",
|
||||
"amazon_fwds": "Amazon a transmis des adresses e-mail",
|
||||
"amazon_days": "Jours en arrière pour vérifier les emails Amazon"
|
||||
},
|
||||
"description": "Veuillez entrer le domaine à partir duquel Amazon envoie des emails (par exemple : amazon.com ou amazon.de)\n\nSi vous utilisez des emails transférés par Amazon, veuillez séparer chaque adresse par une virgule ou entrer (aucun) pour effacer ce paramètre.\n\nRemarque : si vous avez configuré un en-tête de transfert dans l'étape précédente, le champ d'adresse de transfert n'apparaîtra pas — l'en-tête gère automatiquement la correspondance des e-mails Amazon.",
|
||||
"title": "Paramètres Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Répertoire pour stocker les images"
|
||||
},
|
||||
"description": "Veuillez entrer le répertoire dans lequel vous souhaitez que vos images soient stockées.\nLa valeur par défaut est automatiquement remplie.",
|
||||
"title": "Emplacement de stockage des images"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Répertoire pour stocker les images"
|
||||
},
|
||||
"description": "Veuillez entrer le répertoire dans lequel vous souhaitez que vos images soient stockées.\nLa valeur par défaut est automatiquement remplie.",
|
||||
"title": "Emplacement de stockage des images"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "En-tête de transfert (ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Adresses e-mail transférées"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si votre service de transfert inclut l'expéditeur original dans un en-tête, entrez le nom de l'en-tête ici. Cela a la priorité sur la liste d'adresses ci-dessous. Entrez '(none)' ou laissez vide pour utiliser la liste d'adresses à la place.",
|
||||
"forwarded_emails": "Liste séparée par des virgules des adresses de transfert dont proviendront les messages. Laissez '(none)' si vous utilisez l'en-tête de transfert ci-dessus."
|
||||
},
|
||||
"description": "Choisissez comment faire correspondre les e-mails transférés.\n\n**Option 1 — En-tête de transfert** : Si votre service (ex. SimpleLogin) inclut l'expéditeur original dans un en-tête personnalisé, entrez ce nom d'en-tête. Mail and Packages l'utilisera pour faire correspondre automatiquement les adresses des transporteurs.\n\n**Option 2 — Liste d'adresses** : Entrez les adresses de transfert que votre service a attribuées à chaque transporteur, séparées par des virgules.",
|
||||
"title": "Paramètres des e-mails transférés"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "En-tête de transfert (ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Adresses e-mail transférées"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Si votre service de transfert inclut l'expéditeur original dans un en-tête, entrez le nom de l'en-tête ici. Cela a la priorité sur la liste d'adresses ci-dessous. Entrez '(none)' ou laissez vide pour utiliser la liste d'adresses à la place.",
|
||||
"forwarded_emails": "Liste séparée par des virgules des adresses de transfert dont proviendront les messages. Laissez '(none)' si vous utilisez l'en-tête de transfert ci-dessus."
|
||||
},
|
||||
"description": "Choisissez comment faire correspondre les e-mails transférés.\n\n**Option 1 — En-tête de transfert** : Si votre service (ex. SimpleLogin) inclut l'expéditeur original dans un en-tête personnalisé, entrez ce nom d'en-tête. Mail and Packages l'utilisera pour faire correspondre automatiquement les adresses des transporteurs.\n\n**Option 2 — Liste d'adresses** : Entrez les adresses de transfert que votre service a attribuées à chaque transporteur, séparées par des virgules.",
|
||||
"title": "Paramètres des e-mails transférés"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Veuillez entrer les informations de connexion de votre serveur de messagerie.",
|
||||
"data": {
|
||||
"host": "Hôte",
|
||||
"password": "Mot de passe",
|
||||
"port": "Port",
|
||||
"username": "Nom d'utilisateur",
|
||||
"verify_ssl": "Vérifier le certificat SSL",
|
||||
"imap_security": "Sécurité IMAP"
|
||||
},
|
||||
"title": "Courrier et colis (étape 1 sur 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Hôte",
|
||||
"password": "Mot de passe",
|
||||
"port": "Port",
|
||||
"username": "Nom d'utilisateur",
|
||||
"imap_security": "Sécurité IMAP",
|
||||
"verify_ssl": "Vérifier le certificat SSL"
|
||||
},
|
||||
"description": "Veuillez entrer les informations de connexion de votre serveur de messagerie.",
|
||||
"title": "Courrier et colis (étape 1 sur 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Csak a Levelek és a Csomagok egyetlen konfigurációja megengedett.",
|
||||
"reconfigure_successful": "Újrakonfigurálás sikeres"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nem lehet csatlakozni vagy bejelentkezni az e-mail szerverhez. Kérjük, ellenőrizze a naplót a részletekért.",
|
||||
"invalid_path": "Tárolja a képeket egy másik könyvtárban.",
|
||||
"ffmpeg_not_found": "Az MP4 generálása ffmpeg-et igényel",
|
||||
"amazon_domain": "Érvénytelen továbbítási e-mail cím.",
|
||||
"file_not_found": "Képfájl nem található",
|
||||
"invalid_email_format": "Érvénytelen e-mail cím formátum.",
|
||||
"missing_forwarded_emails": "Hiányzó továbbított e-mail címek. Megadhatja a '(none)' értéket a beállítás törléséhez.",
|
||||
"path_not_found": "Könyvtár nem található",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Mail mappa",
|
||||
"resources": "Érzékelők listája",
|
||||
"scan_interval": "Beolvasási időköz (perc)",
|
||||
"image_path": "Kép elérési útja",
|
||||
"gif_duration": "Kép időtartama (másodpercben)",
|
||||
"image_security": "Véletlen képfájlnév",
|
||||
"imap_timeout": "A kapcsolat időtúllépése előtti idő másodpercben (másodperc, minimum 10)",
|
||||
"generate_mp4": "Készítsen mp4-et a képekből",
|
||||
"allow_external": "Kép létrehozása értesítési alkalmazásokhoz",
|
||||
"custom_img": "Használjon egyéni USPS 'nincs posta' képet?",
|
||||
"amazon_custom_img": "Használjon egyéni Amazon 'nincs szállítás' képet?",
|
||||
"ups_custom_img": "Használjon egyéni UPS 'nincs szállítás' képet?",
|
||||
"walmart_custom_img": "Használjon egyéni Walmart 'nincs szállítás' képet?",
|
||||
"fedex_custom_img": "Használjon egyéni FedEx 'nincs szállítás' képet?",
|
||||
"generic_custom_img": "Használjon egyéni általános 'nincs szállítás' képet?",
|
||||
"generate_grid": "Kép rács létrehozása LLM látásmodellekhez",
|
||||
"allow_forwarded_emails": "Engedélyezze a továbbított e-maileket a szolgáltatás alapértelmezett értékén kívül (pl. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Végezze el a konfigurációt az alábbiak testreszabásával az e-mail struktúrája és a Home Assistant telepítése alapján. \n\n A [Levelek és csomagok integrációja] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) opciókkal kapcsolatban tekintse meg a [konfiguráció, sablonok , és az automatizálás szakasz] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) a GitHubon.",
|
||||
"title": "Levél és csomagok (2. lépés a 2-ből)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Egyéni kép elérési útja: (pl.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Adja meg az egyéni \"nincs levél\" kép útvonalát és fájlnevét alább.\n\nPélda: images/custom_nomail.gif",
|
||||
"title": "Levelek és csomagok (3. lépés a 3-ból)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon továbbított e-mail címek",
|
||||
"amazon_days": "Napok visszamenőleg az Amazon e-mailek ellenőrzéséhez"
|
||||
},
|
||||
"description": "Kérjük, adja meg azt a domain-t, ahonnan az Amazon e-maileket küld (pl.: amazon.com vagy amazon.de)\n\nHa az Amazon továbbított e-maileket használ, kérjük, válassza el minden címet vesszővel, vagy írja be, hogy (nincs), hogy törölje ezt a beállítást.\n\nMegjegyzés: ha az előző lépésben beállított egy továbbítási fejlécet, a továbbítási cím mező nem jelenik meg — a fejléc automatikusan kezeli az Amazon e-mailek egyeztetését.",
|
||||
"title": "Amazon beállítások"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Mail mappa",
|
||||
"scan_interval": "Beolvasási időköz (perc)",
|
||||
"image_path": "Kép elérési útja",
|
||||
"gif_duration": "Kép időtartama (másodpercben)",
|
||||
"image_security": "Véletlen képfájlnév",
|
||||
"generate_mp4": "Készítsen mp4-et a képekből",
|
||||
"resources": "Érzékelők listája",
|
||||
"imap_timeout": "A kapcsolat időtúllépése előtti idő másodpercben (másodperc, minimum 10)",
|
||||
"allow_external": "Kép létrehozása értesítési alkalmazásokhoz",
|
||||
"custom_img": "Használjon egyéni USPS 'nincs posta' képet?",
|
||||
"amazon_custom_img": "Használjon egyéni Amazon 'nincs szállítás' képet?",
|
||||
"ups_custom_img": "Használjon egyéni UPS 'nincs szállítás' képet?",
|
||||
"walmart_custom_img": "Használjon egyéni Walmart 'nincs szállítás' képet?",
|
||||
"fedex_custom_img": "Használjon egyéni FedEx 'nincs szállítás' képet?",
|
||||
"generic_custom_img": "Használjon egyéni általános 'nincs szállítás' képet?",
|
||||
"generate_grid": "Kép rács létrehozása LLM látásmodellekhez",
|
||||
"allow_forwarded_emails": "Engedélyezze a továbbított e-maileket a szolgáltatás alapértelmezett értékén kívül (pl. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Fejezze be a konfigurációt a következők testreszabásával az e-mail struktúrája és a Home Assistant telepítése alapján.\n\nA [Mail and Packages integráció](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) lehetőségeinek részleteiről a GitHub-on található [konfiguráció, sablonok és automatizálások szekcióban](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) található információkat.\n\nHa Amazon továbbított e-maileket használ, kérjük, válassza el minden címet vesszővel, vagy írja be a (none) opciót, hogy törölje ezt a beállítást.",
|
||||
"title": "Levél és csomagok (2. lépés a 2-ből)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Egyéni kép elérési útja: (pl.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Adja meg az egyéni \"nincs levél\" kép útvonalát és fájlnevét alább.\n\nPélda: images/custom_nomail.gif",
|
||||
"title": "Levelek és csomagok (3. lépés a 3-ból)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domain",
|
||||
"amazon_fwds": "Amazon továbbított e-mail címek",
|
||||
"amazon_days": "Napok visszamenőleg az Amazon e-mailek ellenőrzéséhez"
|
||||
},
|
||||
"description": "Kérjük, adja meg azt a domain-t, ahonnan az Amazon e-maileket küld (pl.: amazon.com vagy amazon.de)\n\nHa az Amazon továbbított e-maileket használ, kérjük, válassza el minden címet vesszővel, vagy írja be, hogy (nincs), hogy törölje ezt a beállítást.\n\nMegjegyzés: ha az előző lépésben beállított egy továbbítási fejlécet, a továbbítási cím mező nem jelenik meg — a fejléc automatikusan kezeli az Amazon e-mailek egyeztetését.",
|
||||
"title": "Amazon beállítások"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Képek tárolására szolgáló könyvtár"
|
||||
},
|
||||
"description": "Kérjük, adja meg azt a könyvtárat, ahová a képeit szeretné tárolni.\nAz alapértelmezett automatikusan kitöltődik.",
|
||||
"title": "Képtárolási hely"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Képek tárolására szolgáló könyvtár"
|
||||
},
|
||||
"description": "Kérjük, adja meg azt a könyvtárat, ahová a képeit szeretné tárolni.\nAz alapértelmezett automatikusan kitöltődik.",
|
||||
"title": "Képtárolási hely"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Továbbítási fejléc (pl. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Továbbított e-mail címek"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Ha a továbbítási szolgáltatása tartalmazza az eredeti feladót egy fejlécben, adja meg itt a fejléc nevét. Ez elsőbbséget élvez az alábbi címlistával szemben. Írja be a '(none)' értéket, vagy hagyja üresen a címlista használatához.",
|
||||
"forwarded_emails": "Vesszővel elválasztott lista a továbbítási címekről, ahonnan az üzenetek érkeznek. Hagyja '(none)' értéken, ha a fenti továbbítási fejlécet használja."
|
||||
},
|
||||
"description": "Válassza ki, hogyan egyezzen meg a továbbított e-mailekkel.\n\n**1. lehetőség — Továbbítási fejléc**: Ha a szolgáltatása (pl. SimpleLogin) tartalmazza az eredeti feladót egy egyéni fejlécben, adja meg azt a fejlécnevet. A Mail and Packages ezt fogja használni a szállítói címek automatikus egyeztetéséhez.\n\n**2. lehetőség — Címlista**: Adja meg a továbbítási címeket, amelyeket a szolgáltatása minden szállítóhoz hozzárendelt, vesszővel elválasztva.",
|
||||
"title": "Továbbított e-mailek beállításai"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Továbbítási fejléc (pl. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Továbbított e-mail címek"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Ha a továbbítási szolgáltatása tartalmazza az eredeti feladót egy fejlécben, adja meg itt a fejléc nevét. Ez elsőbbséget élvez az alábbi címlistával szemben. Írja be a '(none)' értéket, vagy hagyja üresen a címlista használatához.",
|
||||
"forwarded_emails": "Vesszővel elválasztott lista a továbbítási címekről, ahonnan az üzenetek érkeznek. Hagyja '(none)' értéken, ha a fenti továbbítási fejlécet használja."
|
||||
},
|
||||
"description": "Válassza ki, hogyan egyezzen meg a továbbított e-mailekkel.\n\n**1. lehetőség — Továbbítási fejléc**: Ha a szolgáltatása (pl. SimpleLogin) tartalmazza az eredeti feladót egy egyéni fejlécben, adja meg azt a fejlécnevet. A Mail and Packages ezt fogja használni a szállítói címek automatikus egyeztetéséhez.\n\n**2. lehetőség — Címlista**: Adja meg a továbbítási címeket, amelyeket a szolgáltatása minden szállítóhoz hozzárendelt, vesszővel elválasztva.",
|
||||
"title": "Továbbított e-mailek beállításai"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Kérjük, adja meg a levelezőszervere kapcsolati adatait.",
|
||||
"data": {
|
||||
"host": "Házigazda",
|
||||
"password": "Jelszó",
|
||||
"port": "Kikötő",
|
||||
"username": "Felhasználónév",
|
||||
"verify_ssl": "Ellenőrizze az SSL tanúsítványt",
|
||||
"imap_security": "IMAP Biztonság"
|
||||
},
|
||||
"title": "Levél és csomagok (1. lépés a 2-ből)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Házigazda",
|
||||
"password": "Jelszó",
|
||||
"port": "Kikötő",
|
||||
"username": "Felhasználónév",
|
||||
"imap_security": "IMAP Biztonság",
|
||||
"verify_ssl": "Ellenőrizze az SSL tanúsítványt"
|
||||
},
|
||||
"description": "Kérjük, adja meg a levelezőszervere kapcsolati adatait.",
|
||||
"title": "Levél és csomagok (1. lépés a 2-ből)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "È consentita una sola configurazione di posta e pacchetti.",
|
||||
"reconfigure_successful": "Riconfigurazione Riuscita"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi o accedere al server di posta. Si prega di controllare il registro per i dettagli.",
|
||||
"invalid_path": "Conservare le immagini in un'altra directory.",
|
||||
"ffmpeg_not_found": "Generare MP4 richiede ffmpeg",
|
||||
"amazon_domain": "Indirizzo email di inoltro non valido.",
|
||||
"file_not_found": "File immagine non trovato",
|
||||
"invalid_email_format": "Formato dell'indirizzo email non valido.",
|
||||
"missing_forwarded_emails": "Indirizzi email inoltrati mancanti. Puoi inserire '(none)' per cancellare questa impostazione.",
|
||||
"path_not_found": "Directory non trovato",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Cartella posta",
|
||||
"resources": "Elenco dei sensori",
|
||||
"scan_interval": "Intervallo di scansione (minuti)",
|
||||
"image_path": "Percorso immagine",
|
||||
"gif_duration": "Durata immagine (secondi)",
|
||||
"image_security": "Nome file immagine casuale",
|
||||
"imap_timeout": "Tempo in secondi prima del timeout di connessione (secondi, minimo 10)",
|
||||
"generate_mp4": "Crea mp4 dalle immagini",
|
||||
"allow_external": "Crea immagine per le app di notifica",
|
||||
"custom_img": "Usare l'immagine personalizzata USPS 'nessuna posta'?",
|
||||
"amazon_custom_img": "Usare l'immagine personalizzata Amazon 'nessuna consegna'?",
|
||||
"ups_custom_img": "Usare l'immagine personalizzata UPS 'nessuna consegna'?",
|
||||
"walmart_custom_img": "Usare l'immagine personalizzata Walmart 'nessuna consegna'?",
|
||||
"fedex_custom_img": "Usare l'immagine personalizzata FedEx 'nessuna consegna'?",
|
||||
"generic_custom_img": "Usare l'immagine personalizzata generica 'nessuna consegna'?",
|
||||
"generate_grid": "Crea una griglia di immagini per i modelli di visione LLM",
|
||||
"allow_forwarded_emails": "Consenti email inoltrate oltre al valore predefinito di un servizio (ad es. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Termina la configurazione personalizzando quanto segue in base alla struttura della tua e-mail e all'installazione di Home Assistant. \n\n Per i dettagli sulle opzioni [Integrazione posta e pacchetti] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) rivedere le [configurazione, modelli e sezione automazioni] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) su GitHub.",
|
||||
"title": "Posta e pacchi (passaggio 2 di 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Percorso per l'immagine personalizzata: (es: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Inserisci il percorso e il nome del file per la tua immagine personalizzata di nessuna mail qui sotto.\n\nEsempio: images/custom_nomail.gif",
|
||||
"title": "Posta e Pacchetti (Passaggio 3 di 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Dominio Amazon",
|
||||
"amazon_fwds": "Amazon ha inoltrato gli indirizzi email",
|
||||
"amazon_days": "Giorni precedenti da controllare per le email di Amazon"
|
||||
},
|
||||
"description": "Inserisci il dominio da cui Amazon invia le email (ad esempio: amazon.com o amazon.de)\n\nSe stai utilizzando email inoltrate da Amazon, separa ogni indirizzo con una virgola o inserisci (nessuno) per cancellare questa impostazione.\n\nNota: se hai configurato un'intestazione di inoltro nel passaggio precedente, il campo dell'indirizzo di inoltro non apparirà — l'intestazione gestisce automaticamente la corrispondenza delle email Amazon.",
|
||||
"title": "Impostazioni Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Cartella posta",
|
||||
"scan_interval": "Intervallo di scansione (minuti)",
|
||||
"image_path": "Percorso immagine",
|
||||
"gif_duration": "Durata immagine (secondi)",
|
||||
"image_security": "Nome file immagine casuale",
|
||||
"generate_mp4": "Crea mp4 dalle immagini",
|
||||
"resources": "Elenco dei sensori",
|
||||
"imap_timeout": "Tempo in secondi prima del timeout di connessione (secondi, minimo 10)",
|
||||
"allow_external": "Crea immagine per le app di notifica",
|
||||
"custom_img": "Usare l'immagine personalizzata USPS 'nessuna posta'?",
|
||||
"amazon_custom_img": "Usare l'immagine personalizzata Amazon 'nessuna consegna'?",
|
||||
"ups_custom_img": "Usare l'immagine personalizzata UPS 'nessuna consegna'?",
|
||||
"walmart_custom_img": "Usare l'immagine personalizzata Walmart 'nessuna consegna'?",
|
||||
"fedex_custom_img": "Usare l'immagine personalizzata FedEx 'nessuna consegna'?",
|
||||
"generic_custom_img": "Usare l'immagine personalizzata generica 'nessuna consegna'?",
|
||||
"generate_grid": "Crea una griglia di immagini per i modelli di visione LLM",
|
||||
"allow_forwarded_emails": "Consenti email inoltrate oltre al valore predefinito di un servizio (ad es. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Termina la configurazione personalizzando quanto segue in base alla struttura della tua email e all'installazione di Home Assistant.\n\nPer i dettagli sulle opzioni di [integrazione Mail e Pacchetti](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) consulta la [sezione configurazione, modelli e automazioni](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) su GitHub.\n\nSe utilizzi email inoltrate da Amazon, separa ogni indirizzo con una virgola o inserisci (nessuno) per cancellare questa impostazione.",
|
||||
"title": "Posta e pacchi (passaggio 2 di 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Percorso per l'immagine personalizzata: (ad es.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Inserisci il percorso e il nome del file per la tua immagine personalizzata di nessuna mail qui sotto.\n\nEsempio: images/custom_nomail.gif",
|
||||
"title": "Posta e Pacchetti (Passaggio 3 di 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Dominio Amazon",
|
||||
"amazon_fwds": "Amazon ha inoltrato gli indirizzi email",
|
||||
"amazon_days": "Giorni precedenti da controllare per le email di Amazon"
|
||||
},
|
||||
"description": "Inserisci il dominio da cui Amazon invia le email (ad esempio: amazon.com o amazon.de)\n\nSe stai utilizzando email inoltrate da Amazon, separa ogni indirizzo con una virgola o inserisci (nessuno) per cancellare questa impostazione.\n\nNota: se hai configurato un'intestazione di inoltro nel passaggio precedente, il campo dell'indirizzo di inoltro non apparirà — l'intestazione gestisce automaticamente la corrispondenza delle email Amazon.",
|
||||
"title": "Impostazioni Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Cartella per memorizzare le immagini"
|
||||
},
|
||||
"description": "Inserisci la directory in cui desideri che le tue immagini vengano memorizzate.\nIl valore predefinito viene popolato automaticamente.",
|
||||
"title": "Posizione di archiviazione delle immagini"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Cartella per memorizzare le immagini"
|
||||
},
|
||||
"description": "Inserisci la directory in cui desideri che le tue immagini vengano memorizzate.\nIl valore predefinito viene popolato automaticamente.",
|
||||
"title": "Posizione di archiviazione delle immagini"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Intestazione di inoltro (es. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Indirizzi email inoltrati"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Se il tuo servizio di inoltro include il mittente originale in un'intestazione, inserisci qui il nome dell'intestazione. Questo ha la precedenza sull'elenco di indirizzi seguente. Inserisci '(none)' o lascia vuoto per usare l'elenco di indirizzi.",
|
||||
"forwarded_emails": "Elenco separato da virgole degli indirizzi di inoltro da cui arriveranno i messaggi. Lascia '(none)' se stai usando l'intestazione di inoltro sopra."
|
||||
},
|
||||
"description": "Scegli come abbinare le email inoltrate.\n\n**Opzione 1 — Intestazione di inoltro**: Se il tuo servizio (es. SimpleLogin) include il mittente originale in un'intestazione personalizzata, inserisci il nome di quell'intestazione. Mail and Packages lo utilizzerà per abbinare automaticamente gli indirizzi dei corrieri.\n\n**Opzione 2 — Elenco di indirizzi**: Inserisci gli indirizzi di inoltro che il tuo servizio ha assegnato a ciascun corriere, separati da virgole.",
|
||||
"title": "Impostazioni email inoltrate"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Intestazione di inoltro (es. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Indirizzi email inoltrati"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Se il tuo servizio di inoltro include il mittente originale in un'intestazione, inserisci qui il nome dell'intestazione. Questo ha la precedenza sull'elenco di indirizzi seguente. Inserisci '(none)' o lascia vuoto per usare l'elenco di indirizzi.",
|
||||
"forwarded_emails": "Elenco separato da virgole degli indirizzi di inoltro da cui arriveranno i messaggi. Lascia '(none)' se stai usando l'intestazione di inoltro sopra."
|
||||
},
|
||||
"description": "Scegli come abbinare le email inoltrate.\n\n**Opzione 1 — Intestazione di inoltro**: Se il tuo servizio (es. SimpleLogin) include il mittente originale in un'intestazione personalizzata, inserisci il nome di quell'intestazione. Mail and Packages lo utilizzerà per abbinare automaticamente gli indirizzi dei corrieri.\n\n**Opzione 2 — Elenco di indirizzi**: Inserisci gli indirizzi di inoltro che il tuo servizio ha assegnato a ciascun corriere, separati da virgole.",
|
||||
"title": "Impostazioni email inoltrate"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Inserisci le informazioni di connessione del tuo server di posta.",
|
||||
"data": {
|
||||
"host": "Ospite",
|
||||
"password": "Parola d'ordine",
|
||||
"port": "Porta",
|
||||
"username": "Nome utente",
|
||||
"verify_ssl": "Verifica Certificato SSL",
|
||||
"imap_security": "Sicurezza IMAP"
|
||||
},
|
||||
"title": "Posta e pacchi (passaggio 1 di 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Ospite",
|
||||
"password": "Parola d'ordine",
|
||||
"port": "Porta",
|
||||
"username": "Nome utente",
|
||||
"imap_security": "Sicurezza IMAP",
|
||||
"verify_ssl": "Verifica Certificato SSL"
|
||||
},
|
||||
"description": "Inserisci le informazioni di connessione del tuo server di posta.",
|
||||
"title": "Posta e pacchi (passaggio 1 di 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "메일 및 패키지의 단일 구성 만 허용됩니다.",
|
||||
"reconfigure_successful": "재구성 성공"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "메일 서버에 연결하거나 로그인 할 수 없습니다. 자세한 내용은 로그를 확인하십시오.",
|
||||
"invalid_path": "이미지를 다른 디렉토리에 저장하십시오.",
|
||||
"ffmpeg_not_found": "MP4 생성은 ffmpeg가 필요합니다",
|
||||
"amazon_domain": "잘못된 전달 이메일 주소입니다.",
|
||||
"file_not_found": "이미지 파일을 찾을 수 없습니다",
|
||||
"invalid_email_format": "잘못된 이메일 주소 형식입니다.",
|
||||
"missing_forwarded_emails": "전달된 이메일 주소가 없습니다. 이 설정을 지우려면 '(none)'을 입력할 수 있습니다.",
|
||||
"path_not_found": "디렉토리를 찾을 수 없습니다",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "메일 폴더",
|
||||
"resources": "센서 목록",
|
||||
"scan_interval": "스캔 간격 (분)",
|
||||
"image_path": "이미지 경로",
|
||||
"gif_duration": "이미지 지속 시간 (초)",
|
||||
"image_security": "임의의 이미지 파일 이름",
|
||||
"imap_timeout": "연결 시간 초과 전 시간(초, 최소 10)",
|
||||
"generate_mp4": "이미지에서 mp4 만들기",
|
||||
"allow_external": "알림 앱용 이미지 생성",
|
||||
"custom_img": "사용자 정의 USPS '우편 없음' 이미지를 사용하시겠습니까?",
|
||||
"amazon_custom_img": "사용자 정의 Amazon '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"ups_custom_img": "사용자 정의 UPS '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"walmart_custom_img": "사용자 정의 Walmart '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"fedex_custom_img": "사용자 정의 FedEx '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"generic_custom_img": "사용자 정의 일반 '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"generate_grid": "LLM 비전 모델을 위한 이미지 그리드 생성",
|
||||
"allow_forwarded_emails": "서비스 기본값 외에 전달된 이메일 허용 (예: no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "이메일 구조 및 Home Assistant 설치에 따라 다음을 사용자 정의하여 구성을 완료하십시오. \n\n [메일 및 패키지 통합] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) 옵션에 대한 자세한 내용은 [구성, 템플릿 및 자동화 섹션] (https://github.com/moralmunky/Home-Assistant-Mail-and-Packages/wiki/Configuration-and-Email-Settings#configuration)",
|
||||
"title": "메일 및 패키지 (2/2 단계)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "사용자 정의 이미지 경로: (예: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "아래에 사용자 정의 '메일 없음' 이미지의 경로와 파일 이름을 입력하세요.\n\n예시: images/custom_nomail.gif",
|
||||
"title": "우편 및 패키지 (3단계 중 3단계)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "아마존 도메인",
|
||||
"amazon_fwds": "Amazon이 전달한 이메일 주소",
|
||||
"amazon_days": "Amazon 이메일을 확인하기 위해 돌아가야 할 날짜"
|
||||
},
|
||||
"description": "Amazon이 이메일을 보내는 도메인을 입력해 주세요 (예: amazon.com 또는 amazon.de)\n\nAmazon에서 전달된 이메일을 사용하는 경우 각 주소를 쉼표로 구분하거나 이 설정을 지우려면 (none)을 입력하세요.\n\n참고: 이전 단계에서 전달 헤더를 구성한 경우 전달 주소 필드가 표시되지 않습니다 — 헤더가 Amazon 이메일 매칭을 자동으로 처리합니다.",
|
||||
"title": "아마존 설정"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "메일 폴더",
|
||||
"scan_interval": "스캔 간격 (분)",
|
||||
"image_path": "이미지 경로",
|
||||
"gif_duration": "이미지 지속 시간 (초)",
|
||||
"image_security": "임의의 이미지 파일 이름",
|
||||
"generate_mp4": "이미지에서 mp4 만들기",
|
||||
"resources": "센서 목록",
|
||||
"imap_timeout": "연결 시간 초과 전 시간(초, 최소 10)",
|
||||
"allow_external": "알림 앱용 이미지 생성",
|
||||
"custom_img": "사용자 정의 USPS '우편 없음' 이미지를 사용하시겠습니까?",
|
||||
"amazon_custom_img": "사용자 정의 Amazon '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"ups_custom_img": "사용자 정의 UPS '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"walmart_custom_img": "사용자 정의 Walmart '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"fedex_custom_img": "사용자 정의 FedEx '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"generic_custom_img": "사용자 정의 일반 '배송 없음' 이미지를 사용하시겠습니까?",
|
||||
"generate_grid": "LLM 비전 모델을 위한 이미지 그리드 생성",
|
||||
"allow_forwarded_emails": "서비스 기본값 외에 전달된 이메일 허용 (예: no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "이메일 구조와 Home Assistant 설치에 따라 다음을 사용자 정의하여 구성을 완료하십시오.\n\n[Mail and Packages 통합](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) 옵션에 대한 자세한 내용은 GitHub의 [구성, 템플릿, 자동화 섹션](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration)을 참조하십시오.\n\nAmazon에서 전달된 이메일을 사용하는 경우 각 주소를 쉼표로 구분하거나 이 설정을 지우려면 (none)을 입력하십시오.",
|
||||
"title": "메일 및 패키지 (2/2 단계)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "사용자 정의 이미지 경로: (예: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "아래에 사용자 정의 '메일 없음' 이미지의 경로와 파일 이름을 입력하세요.\n\n예시: images/custom_nomail.gif",
|
||||
"title": "우편 및 패키지 (3단계 중 3단계)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "아마존 도메인",
|
||||
"amazon_fwds": "Amazon이 전달한 이메일 주소",
|
||||
"amazon_days": "Amazon 이메일을 확인하기 위해 돌아가야 할 날짜"
|
||||
},
|
||||
"description": "Amazon이 이메일을 보내는 도메인을 입력해 주세요 (예: amazon.com 또는 amazon.de)\n\nAmazon에서 전달된 이메일을 사용하는 경우 각 주소를 쉼표로 구분하거나 이 설정을 지우려면 (none)을 입력하세요.\n\n참고: 이전 단계에서 전달 헤더를 구성한 경우 전달 주소 필드가 표시되지 않습니다 — 헤더가 Amazon 이메일 매칭을 자동으로 처리합니다.",
|
||||
"title": "아마존 설정"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "이미지를 저장할 디렉토리"
|
||||
},
|
||||
"description": "이미지를 저장할 디렉토리를 입력해 주세요.\n기본값은 자동으로 채워집니다.",
|
||||
"title": "이미지 저장 위치"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "이미지를 저장할 디렉토리"
|
||||
},
|
||||
"description": "이미지를 저장할 디렉토리를 입력해 주세요.\n기본값은 자동으로 채워집니다.",
|
||||
"title": "이미지 저장 위치"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "전달 헤더 (예: X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "전달된 이메일 주소"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "전달 서비스가 헤더에 원래 발신자를 포함하는 경우, 여기에 헤더 이름을 입력하세요. 이는 아래 주소 목록보다 우선합니다. 주소 목록을 사용하려면 '(none)'을 입력하거나 비워두세요.",
|
||||
"forwarded_emails": "메시지가 도착할 전달 주소의 쉼표로 구분된 목록입니다. 위의 전달 헤더를 사용하는 경우 '(none)'으로 두세요."
|
||||
},
|
||||
"description": "전달된 이메일을 일치시키는 방법을 선택하세요.\n\n**옵션 1 — 전달 헤더**: 서비스(예: SimpleLogin)가 사용자 지정 헤더에 원래 발신자를 포함하는 경우, 해당 헤더 이름을 입력하세요. Mail and Packages가 이를 사용하여 배송사 주소를 자동으로 일치시킵니다.\n\n**옵션 2 — 주소 목록**: 서비스가 각 배송사에 할당한 전달 주소를 쉼표로 구분하여 입력하세요.",
|
||||
"title": "전달 이메일 설정"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "전달 헤더 (예: X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "전달된 이메일 주소"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "전달 서비스가 헤더에 원래 발신자를 포함하는 경우, 여기에 헤더 이름을 입력하세요. 이는 아래 주소 목록보다 우선합니다. 주소 목록을 사용하려면 '(none)'을 입력하거나 비워두세요.",
|
||||
"forwarded_emails": "메시지가 도착할 전달 주소의 쉼표로 구분된 목록입니다. 위의 전달 헤더를 사용하는 경우 '(none)'으로 두세요."
|
||||
},
|
||||
"description": "전달된 이메일을 일치시키는 방법을 선택하세요.\n\n**옵션 1 — 전달 헤더**: 서비스(예: SimpleLogin)가 사용자 지정 헤더에 원래 발신자를 포함하는 경우, 해당 헤더 이름을 입력하세요. Mail and Packages가 이를 사용하여 배송사 주소를 자동으로 일치시킵니다.\n\n**옵션 2 — 주소 목록**: 서비스가 각 배송사에 할당한 전달 주소를 쉼표로 구분하여 입력하세요.",
|
||||
"title": "전달 이메일 설정"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "메일 서버의 연결 정보를 입력해 주세요.",
|
||||
"data": {
|
||||
"host": "주최자",
|
||||
"password": "암호",
|
||||
"port": "포트",
|
||||
"username": "사용자 이름",
|
||||
"verify_ssl": "SSL 인증서 확인",
|
||||
"imap_security": "IMAP 보안"
|
||||
},
|
||||
"title": "메일 및 패키지 (1/2 단계)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "주최자",
|
||||
"password": "암호",
|
||||
"port": "포트",
|
||||
"username": "사용자 이름",
|
||||
"imap_security": "IMAP 보안",
|
||||
"verify_ssl": "SSL 인증서 확인"
|
||||
},
|
||||
"description": "메일 서버의 연결 정보를 입력해 주세요.",
|
||||
"title": "메일 및 패키지 (1/2 단계)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Slechts een enkele configuratie van Mail en pakketten is toegestaan.",
|
||||
"reconfigure_successful": "Herconfiguratie succesvol"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan geen verbinding maken met of inloggen op de mailserver. Controleer het logboek voor details.",
|
||||
"invalid_path": "Bewaar de afbeeldingen in een andere map.",
|
||||
"ffmpeg_not_found": "MP4 genereren vereist ffmpeg",
|
||||
"amazon_domain": "Ongeldig doorstuur e-mailadres.",
|
||||
"file_not_found": "Afbeeldingsbestand niet gevonden",
|
||||
"invalid_email_format": "Ongeldig e-mailadres formaat.",
|
||||
"missing_forwarded_emails": "Ontbrekende doorgestuurde e-mailadressen. U kunt '(none)' invoeren om deze instelling te wissen.",
|
||||
"path_not_found": "Map niet gevonden",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "E-mailmap",
|
||||
"resources": "Lijst van sensoren",
|
||||
"scan_interval": "Scaninterval (minuten)",
|
||||
"image_path": "Afbeeldingspad",
|
||||
"gif_duration": "Beeldduur (seconden)",
|
||||
"image_security": "Bestandsnaam willekeurige afbeelding",
|
||||
"imap_timeout": "Tijd in seconden voordat de verbinding verloopt (seconden, minimaal 10)",
|
||||
"generate_mp4": "Maak mp4 van afbeeldingen",
|
||||
"allow_external": "Maak afbeelding voor meldingsapps",
|
||||
"custom_img": "Gebruik aangepaste USPS 'geen post' afbeelding?",
|
||||
"amazon_custom_img": "Gebruik aangepaste Amazon 'geen levering' afbeelding?",
|
||||
"ups_custom_img": "Gebruik aangepaste UPS 'geen levering' afbeelding?",
|
||||
"walmart_custom_img": "Gebruik aangepaste Walmart 'geen levering' afbeelding?",
|
||||
"fedex_custom_img": "Gebruik aangepaste FedEx 'geen levering' afbeelding?",
|
||||
"generic_custom_img": "Gebruik aangepaste generieke 'geen levering' afbeelding?",
|
||||
"generate_grid": "Maak een afbeeldingsraster voor LLM-visiemodellen",
|
||||
"allow_forwarded_emails": "Doorgestuurde e-mails toestaan naast de standaardwaarde van een service (bijv. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Voltooi de configuratie door het volgende aan te passen op basis van uw e-mailstructuur en Home Assistant-installatie. \n\n Voor meer informatie over de [E-mail en pakketten integratie] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) opties bekijk de [configuratie, sjablonen , en automatisering sectie] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) op GitHub.",
|
||||
"title": "Mail en pakketten (stap 2 van 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Pad naar aangepaste afbeelding: (bijv: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Voer hieronder het pad en de bestandsnaam in voor uw aangepaste afbeelding voor geen mail.\n\nVoorbeeld: images/custom_nomail.gif",
|
||||
"title": "Mail en Pakketten (Stap 3 van 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domein",
|
||||
"amazon_fwds": "Amazon stuurde e-mailadressen door",
|
||||
"amazon_days": "Dagen terug om te controleren op Amazon e-mails"
|
||||
},
|
||||
"description": "Voer het domein in waarvan Amazon e-mails verstuurt (bijv: amazon.com of amazon.de)\n\nAls u doorgestuurde e-mails van Amazon gebruikt, scheid dan elk adres met een komma of voer (geen) in om deze instelling te wissen.\n\nOpmerking: als u in de vorige stap een doorstuurheader hebt geconfigureerd, verschijnt het doorstuuradresveld niet — de header verwerkt de Amazon e-mailkoppeling automatisch.",
|
||||
"title": "Amazon Instellingen"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "E-mailmap",
|
||||
"scan_interval": "Scaninterval (minuten)",
|
||||
"image_path": "Afbeeldingspad",
|
||||
"gif_duration": "Beeldduur (seconden)",
|
||||
"image_security": "Bestandsnaam willekeurige afbeelding",
|
||||
"generate_mp4": "Maak mp4 van afbeeldingen",
|
||||
"resources": "Lijst van sensoren",
|
||||
"imap_timeout": "Tijd in seconden voordat de verbinding verloopt (seconden, minimaal 10)",
|
||||
"allow_external": "Maak afbeelding voor meldingsapps",
|
||||
"custom_img": "Gebruik aangepaste USPS 'geen post' afbeelding?",
|
||||
"amazon_custom_img": "Gebruik aangepaste Amazon 'geen levering' afbeelding?",
|
||||
"ups_custom_img": "Gebruik aangepaste UPS 'geen levering' afbeelding?",
|
||||
"walmart_custom_img": "Gebruik aangepaste Walmart 'geen levering' afbeelding?",
|
||||
"fedex_custom_img": "Gebruik aangepaste FedEx 'geen levering' afbeelding?",
|
||||
"generic_custom_img": "Gebruik aangepaste generieke 'geen levering' afbeelding?",
|
||||
"generate_grid": "Maak een afbeeldingsraster voor LLM-visiemodellen",
|
||||
"allow_forwarded_emails": "Doorgestuurde e-mails toestaan naast de standaardwaarde van een service (bijv. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Rond de configuratie af door het volgende aan te passen op basis van uw e-mailstructuur en Home Assistant-installatie.\n\nVoor details over de [Mail en Packages integratie](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) opties, bekijk de [configuratie, sjablonen en automatiseringen sectie](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) op GitHub.\n\nAls u doorgestuurde e-mails van Amazon gebruikt, scheid dan elk adres met een komma of voer (geen) in om deze instelling te wissen.",
|
||||
"title": "Mail en pakketten (stap 2 van 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Pad naar aangepaste afbeelding: (bijv.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Voer hieronder het pad en de bestandsnaam in voor uw aangepaste afbeelding voor geen mail.\n\nVoorbeeld: images/custom_nomail.gif",
|
||||
"title": "Mail en Pakketten (Stap 3 van 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domein",
|
||||
"amazon_fwds": "Amazon stuurde e-mailadressen door",
|
||||
"amazon_days": "Dagen terug om te controleren op Amazon e-mails"
|
||||
},
|
||||
"description": "Voer het domein in waarvan Amazon e-mails verstuurt (bijv: amazon.com of amazon.de)\n\nAls u doorgestuurde e-mails van Amazon gebruikt, scheid dan elk adres met een komma of voer (geen) in om deze instelling te wissen.\n\nOpmerking: als u in de vorige stap een doorstuurheader hebt geconfigureerd, verschijnt het doorstuuradresveld niet — de header verwerkt de Amazon e-mailkoppeling automatisch.",
|
||||
"title": "Amazon Instellingen"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Map om afbeeldingen op te slaan"
|
||||
},
|
||||
"description": "Voer de map in waar u uw afbeeldingen wilt opslaan.\nDe standaard is automatisch ingevuld.",
|
||||
"title": "Locatie voor afbeeldingsopslag"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Map om afbeeldingen op te slaan"
|
||||
},
|
||||
"description": "Voer de map in waar u uw afbeeldingen wilt opslaan.\nDe standaard is automatisch ingevuld.",
|
||||
"title": "Locatie voor afbeeldingsopslag"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Doorstuurkoptekst (bijv. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Doorgestuurde e-mailadressen"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Als uw doorstuurdienst de oorspronkelijke afzender in een koptekst opneemt, voer dan hier de naam van de koptekst in. Dit heeft voorrang op de onderstaande adreslijst. Voer '(none)' in of laat leeg om de adreslijst te gebruiken.",
|
||||
"forwarded_emails": "Kommagescheiden lijst van doorstuuradressen waarvan berichten zullen aankomen. Laat '(none)' staan als u de bovenstaande doorstuurkoptekst gebruikt."
|
||||
},
|
||||
"description": "Kies hoe doorgestuurde e-mails worden gekoppeld.\n\n**Optie 1 — Doorstuurkoptekst**: Als uw dienst (bijv. SimpleLogin) de oorspronkelijke afzender in een aangepaste koptekst opneemt, voer dan die koptekstnaam in. Mail and Packages zal die gebruiken om vervoerdersadressen automatisch te koppelen.\n\n**Optie 2 — Adreslijst**: Voer de doorstuuradressen in die uw dienst aan elke vervoerder heeft toegewezen, gescheiden door komma's.",
|
||||
"title": "Instellingen doorgestuurde e-mails"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Doorstuurkoptekst (bijv. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Doorgestuurde e-mailadressen"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Als uw doorstuurdienst de oorspronkelijke afzender in een koptekst opneemt, voer dan hier de naam van de koptekst in. Dit heeft voorrang op de onderstaande adreslijst. Voer '(none)' in of laat leeg om de adreslijst te gebruiken.",
|
||||
"forwarded_emails": "Kommagescheiden lijst van doorstuuradressen waarvan berichten zullen aankomen. Laat '(none)' staan als u de bovenstaande doorstuurkoptekst gebruikt."
|
||||
},
|
||||
"description": "Kies hoe doorgestuurde e-mails worden gekoppeld.\n\n**Optie 1 — Doorstuurkoptekst**: Als uw dienst (bijv. SimpleLogin) de oorspronkelijke afzender in een aangepaste koptekst opneemt, voer dan die koptekstnaam in. Mail and Packages zal die gebruiken om vervoerdersadressen automatisch te koppelen.\n\n**Optie 2 — Adreslijst**: Voer de doorstuuradressen in die uw dienst aan elke vervoerder heeft toegewezen, gescheiden door komma's.",
|
||||
"title": "Instellingen doorgestuurde e-mails"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Vul de verbindingsinformatie van uw mailserver in.",
|
||||
"data": {
|
||||
"host": "Gastheer",
|
||||
"password": "Wachtwoord",
|
||||
"port": "Haven",
|
||||
"username": "Gebruikersnaam",
|
||||
"verify_ssl": "SSL Certificaat Verifiëren",
|
||||
"imap_security": "IMAP-beveiliging"
|
||||
},
|
||||
"title": "Mail en pakketten (stap 1 van 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Gastheer",
|
||||
"password": "Wachtwoord",
|
||||
"port": "Haven",
|
||||
"username": "Gebruikersnaam",
|
||||
"imap_security": "IMAP-beveiliging",
|
||||
"verify_ssl": "SSL Certificaat Verifiëren"
|
||||
},
|
||||
"description": "Vul de verbindingsinformatie van uw mailserver in.",
|
||||
"title": "Mail en pakketten (stap 1 van 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Bare en enkelt konfigurasjon av e-post og pakker er tillatt.",
|
||||
"reconfigure_successful": "Omkonfigurering vellykket"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan ikke koble til eller logge inn på postserveren. Vennligst sjekk loggen for detaljer.",
|
||||
"invalid_path": "Lagre bildene i en annen katalog.",
|
||||
"ffmpeg_not_found": "Generer MP4 krever ffmpeg",
|
||||
"amazon_domain": "Ugyldig videresendings e-postadresse.",
|
||||
"file_not_found": "Bildefil ikke funnet",
|
||||
"invalid_email_format": "Ugyldig e-postadresseformat.",
|
||||
"missing_forwarded_emails": "Manglende videresendte e-postadresser. Du kan skrive inn '(none)' for å tømme denne innstillingen.",
|
||||
"path_not_found": "Mappen ble ikke funnet",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "E-postmappe",
|
||||
"resources": "Sensorliste",
|
||||
"scan_interval": "Skanneintervall (minutter)",
|
||||
"image_path": "Bildevei",
|
||||
"gif_duration": "Bildets varighet (sekunder)",
|
||||
"image_security": "Tilfeldig bilde filnavn",
|
||||
"imap_timeout": "Tid i sekunder før tilkoblingen tidsavbrudd (sekunder, minimum 10)",
|
||||
"generate_mp4": "Lag mp4 fra bilder",
|
||||
"allow_external": "Lag bilde for varslingsapper",
|
||||
"custom_img": "Bruk tilpasset USPS 'ingen post' bilde?",
|
||||
"amazon_custom_img": "Bruk tilpasset Amazon 'ingen levering' bilde?",
|
||||
"ups_custom_img": "Bruk tilpasset UPS 'ingen levering' bilde?",
|
||||
"walmart_custom_img": "Bruk tilpasset Walmart 'ingen levering' bilde?",
|
||||
"fedex_custom_img": "Bruk tilpasset FedEx 'ingen levering' bilde?",
|
||||
"generic_custom_img": "Bruk tilpasset generisk 'ingen levering' bilde?",
|
||||
"generate_grid": "Opprett bildegitter for LLM-visjonsmodeller",
|
||||
"allow_forwarded_emails": "Tillat videresendte e-poster i tillegg til en tjenestes standardverdi (f.eks. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Fullfør konfigurasjonen ved å tilpasse følgende basert på e-poststrukturen og Home Assistant-installasjonen. \n\n For detaljer om alternativene [Mail and Packages] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), les gjennom [konfigurasjon, maler , og automatiseringsdel] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) på GitHub.",
|
||||
"title": "E-post og pakker (trinn 2 av 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Sti til tilpasset bilde: (f.eks: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Skriv inn stien og filnavnet til ditt tilpassede ingen post-bilde nedenfor.\n\nEksempel: bilder/tilpasset_ingenpost.gif",
|
||||
"title": "Post og pakker (Trinn 3 av 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon-domene",
|
||||
"amazon_fwds": "Amazon videresendte e-postadresser",
|
||||
"amazon_days": "Dager tilbake for å sjekke Amazon-e-poster"
|
||||
},
|
||||
"description": "Vennligst skriv inn domenet amazon sender e-poster fra (f.eks: amazon.com eller amazon.de)\n\nHvis du bruker Amazon videresendte e-poster, vennligst skill hver adresse med et komma, eller skriv inn (ingen) for å tømme denne innstillingen.\n\nMerk: hvis du konfigurerte et videresendingshode i forrige trinn, vil feltet for videresendingsadresse ikke vises — hodet håndterer Amazon e-postkoblingen automatisk.",
|
||||
"title": "Amazon-innstillinger"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "E-postmappe",
|
||||
"scan_interval": "Skanneintervall (minutter)",
|
||||
"image_path": "Bildevei",
|
||||
"gif_duration": "Bildets varighet (sekunder)",
|
||||
"image_security": "Tilfeldig bilde filnavn",
|
||||
"generate_mp4": "Lag mp4 fra bilder",
|
||||
"resources": "Sensorliste",
|
||||
"imap_timeout": "Tid i sekunder før tilkoblingen tidsavbrudd (sekunder, minimum 10)",
|
||||
"allow_external": "Lag bilde for varslingsapper",
|
||||
"custom_img": "Bruk tilpasset USPS 'ingen post' bilde?",
|
||||
"amazon_custom_img": "Bruk tilpasset Amazon 'ingen levering' bilde?",
|
||||
"ups_custom_img": "Bruk tilpasset UPS 'ingen levering' bilde?",
|
||||
"walmart_custom_img": "Bruk tilpasset Walmart 'ingen levering' bilde?",
|
||||
"fedex_custom_img": "Bruk tilpasset FedEx 'ingen levering' bilde?",
|
||||
"generic_custom_img": "Bruk tilpasset generisk 'ingen levering' bilde?",
|
||||
"generate_grid": "Opprett bildegitter for LLM-visjonsmodeller",
|
||||
"allow_forwarded_emails": "Tillat videresendte e-poster i tillegg til en tjenestes standardverdi (f.eks. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Fullfør konfigurasjonen ved å tilpasse følgende basert på din e-poststruktur og Home Assistant-installasjon.\n\nFor detaljer om [Mail and Packages-integrasjonen](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) alternativene, se gjennom [konfigurasjon, maler og automatiseringsseksjonen](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) på GitHub.\n\nHvis du bruker Amazon videresendte e-poster, vennligst skill hver adresse med et komma eller skriv inn (ingen) for å tømme denne innstillingen.",
|
||||
"title": "E-post og pakker (trinn 2 av 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Sti til tilpasset bilde: (dvs.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Skriv inn stien og filnavnet til ditt tilpassede ingen post-bilde nedenfor.\n\nEksempel: bilder/tilpasset_ingenpost.gif",
|
||||
"title": "Post og pakker (Trinn 3 av 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon-domene",
|
||||
"amazon_fwds": "Amazon videresendte e-postadresser",
|
||||
"amazon_days": "Dager tilbake for å sjekke Amazon-e-poster"
|
||||
},
|
||||
"description": "Vennligst skriv inn domenet amazon sender e-poster fra (f.eks: amazon.com eller amazon.de)\n\nHvis du bruker Amazon videresendte e-poster, vennligst skill hver adresse med et komma, eller skriv inn (ingen) for å tømme denne innstillingen.\n\nMerk: hvis du konfigurerte et videresendingshode i forrige trinn, vil feltet for videresendingsadresse ikke vises — hodet håndterer Amazon e-postkoblingen automatisk.",
|
||||
"title": "Amazon-innstillinger"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Katalog for å lagre bilder"
|
||||
},
|
||||
"description": "Vennligst oppgi mappen du vil at bildene dine skal lagres i.\nStandard er automatisk fylt ut.",
|
||||
"title": "Bildelagringssted"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Katalog for å lagre bilder"
|
||||
},
|
||||
"description": "Vennligst oppgi mappen du vil at bildene dine skal lagres i.\nStandard er automatisk fylt ut.",
|
||||
"title": "Bildelagringssted"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Videresendingshode (f.eks. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Videresendte e-postadresser"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Hvis videresendingstjenesten din inkluderer den opprinnelige avsenderen i en topptekst, skriv inn topptekstnavnet her. Dette har forrang over adresselisten nedenfor. Skriv inn '(none)' eller la det stå tomt for å bruke adresselisten i stedet.",
|
||||
"forwarded_emails": "Kommaseparert liste over videresendingsadresser meldinger vil komme fra. La '(none)' stå hvis du bruker videresendingshodet ovenfor."
|
||||
},
|
||||
"description": "Velg hvordan videresendte e-poster skal matches.\n\n**Alternativ 1 — Videresendingshode**: Hvis tjenesten din (f.eks. SimpleLogin) inkluderer den opprinnelige avsenderen i et tilpasset hode, skriv inn det hodet. Mail and Packages vil bruke det til å matche fraktadresser automatisk.\n\n**Alternativ 2 — Adresseliste**: Skriv inn videresendingsadressene tjenesten din har tildelt hver fraktfører, atskilt med komma.",
|
||||
"title": "Innstillinger for videresendte e-poster"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Videresendingshode (f.eks. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Videresendte e-postadresser"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Hvis videresendingstjenesten din inkluderer den opprinnelige avsenderen i en topptekst, skriv inn topptekstnavnet her. Dette har forrang over adresselisten nedenfor. Skriv inn '(none)' eller la det stå tomt for å bruke adresselisten i stedet.",
|
||||
"forwarded_emails": "Kommaseparert liste over videresendingsadresser meldinger vil komme fra. La '(none)' stå hvis du bruker videresendingshodet ovenfor."
|
||||
},
|
||||
"description": "Velg hvordan videresendte e-poster skal matches.\n\n**Alternativ 1 — Videresendingshode**: Hvis tjenesten din (f.eks. SimpleLogin) inkluderer den opprinnelige avsenderen i et tilpasset hode, skriv inn det hodet. Mail and Packages vil bruke det til å matche fraktadresser automatisk.\n\n**Alternativ 2 — Adresseliste**: Skriv inn videresendingsadressene tjenesten din har tildelt hver fraktfører, atskilt med komma.",
|
||||
"title": "Innstillinger for videresendte e-poster"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Vennligst oppgi tilkoblingsinformasjonen til din e-postserver.",
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"password": "Passord",
|
||||
"port": "Havn",
|
||||
"username": "Brukernavn",
|
||||
"verify_ssl": "Bekreft SSL-sertifikat",
|
||||
"imap_security": "IMAP-sikkerhet"
|
||||
},
|
||||
"title": "E-post og pakker (trinn 1 av 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"password": "Passord",
|
||||
"port": "Havn",
|
||||
"username": "Brukernavn",
|
||||
"imap_security": "IMAP-sikkerhet",
|
||||
"verify_ssl": "Bekreft SSL-sertifikat"
|
||||
},
|
||||
"description": "Vennligst oppgi tilkoblingsinformasjonen til din e-postserver.",
|
||||
"title": "E-post og pakker (trinn 1 av 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja poczty i pakietów.",
|
||||
"reconfigure_successful": "Pomyślnie przekonfigurowano"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie można połączyć się lub zalogować do serwera pocztowego. Sprawdź szczegóły w dzienniku.",
|
||||
"invalid_path": "Proszę przechowywać obrazy w innym katalogu.",
|
||||
"ffmpeg_not_found": "Generowanie MP4 wymaga ffmpeg",
|
||||
"amazon_domain": "Nieprawidłowy adres e-mail do przekierowania.",
|
||||
"file_not_found": "Nie znaleziono pliku obrazu",
|
||||
"invalid_email_format": "Nieprawidłowy format adresu e-mail.",
|
||||
"missing_forwarded_emails": "Brak przekierowanych adresów e-mail. Możesz wprowadzić '(none)', aby wyczyścić to ustawienie.",
|
||||
"path_not_found": "Katalog nie został znaleziony",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Folder poczty",
|
||||
"resources": "Lista czujników",
|
||||
"scan_interval": "Interwał skanowania (minuty)",
|
||||
"image_path": "Ścieżka obrazu",
|
||||
"gif_duration": "Czas trwania obrazu (sekundy)",
|
||||
"image_security": "Losowa nazwa pliku obrazu",
|
||||
"imap_timeout": "Czas w sekundach przed wygaśnięciem połączenia (sekundy, minimum 10)",
|
||||
"generate_mp4": "Utwórz mp4 z obrazów",
|
||||
"allow_external": "Utwórz obraz dla aplikacji powiadomień",
|
||||
"custom_img": "Użyć niestandardowego obrazu USPS 'brak poczty'?",
|
||||
"amazon_custom_img": "Użyć niestandardowego obrazu Amazon 'brak dostawy'?",
|
||||
"ups_custom_img": "Użyć niestandardowego obrazu UPS 'brak dostawy'?",
|
||||
"walmart_custom_img": "Użyć niestandardowego obrazu Walmart 'brak dostawy'?",
|
||||
"fedex_custom_img": "Użyć niestandardowego obrazu FedEx 'brak dostawy'?",
|
||||
"generic_custom_img": "Użyć niestandardowego obrazu ogólnego 'brak dostawy'?",
|
||||
"generate_grid": "Utwórz siatkę obrazów dla modeli wizji LLM",
|
||||
"allow_forwarded_emails": "Zezwalaj na przekierowane e-maile oprócz wartości domyślnej usługi (np. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Zakończ konfigurację, dostosowując następujące elementy w oparciu o strukturę poczty e-mail i instalację Home Assistant. \n\n Aby uzyskać szczegółowe informacje na temat opcji [Integracja poczty i pakietów] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) sprawdź opcje [konfiguracja, szablony i sekcja automatyzacji] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHub.",
|
||||
"title": "Poczta i paczki (krok 2 z 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Ścieżka do niestandardowego obrazu: (np.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Wprowadź ścieżkę i nazwę pliku do swojego niestandardowego obrazu bez maila poniżej.\n\nPrzykład: images/custom_nomail.gif",
|
||||
"title": "Poczta i paczki (Krok 3 z 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domena Amazon",
|
||||
"amazon_fwds": "Amazon przekazał adresy e-mail",
|
||||
"amazon_days": "Dni do sprawdzenia dla wiadomości e-mail od Amazon"
|
||||
},
|
||||
"description": "Wprowadź domenę, z której Amazon wysyła e-maile (np. amazon.com lub amazon.de)\n\nJeśli korzystasz z przekierowanych e-maili od Amazon, oddziel każdy adres przecinkiem lub wpisz (brak), aby wyczyścić to ustawienie.\n\nUwaga: jeśli w poprzednim kroku skonfigurowałeś nagłówek przekierowania, pole adresu przekierowania nie pojawi się — nagłówek automatycznie obsługuje dopasowywanie e-maili Amazon.",
|
||||
"title": "Ustawienia Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Folder poczty",
|
||||
"scan_interval": "Interwał skanowania (minuty)",
|
||||
"image_path": "Ścieżka obrazu",
|
||||
"gif_duration": "Czas trwania obrazu (sekundy)",
|
||||
"image_security": "Losowa nazwa pliku obrazu",
|
||||
"generate_mp4": "Utwórz mp4 z obrazów",
|
||||
"resources": "Lista czujników",
|
||||
"imap_timeout": "Czas w sekundach przed wygaśnięciem połączenia (sekundy, minimum 10)",
|
||||
"allow_external": "Utwórz obraz dla aplikacji powiadomień",
|
||||
"custom_img": "Użyć niestandardowego obrazu USPS 'brak poczty'?",
|
||||
"amazon_custom_img": "Użyć niestandardowego obrazu Amazon 'brak dostawy'?",
|
||||
"ups_custom_img": "Użyć niestandardowego obrazu UPS 'brak dostawy'?",
|
||||
"walmart_custom_img": "Użyć niestandardowego obrazu Walmart 'brak dostawy'?",
|
||||
"fedex_custom_img": "Użyć niestandardowego obrazu FedEx 'brak dostawy'?",
|
||||
"generic_custom_img": "Użyć niestandardowego obrazu ogólnego 'brak dostawy'?",
|
||||
"generate_grid": "Utwórz siatkę obrazów dla modeli wizji LLM",
|
||||
"allow_forwarded_emails": "Zezwalaj na przekierowane e-maile oprócz wartości domyślnej usługi (np. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Zakończ konfigurację, dostosowując następujące elementy do struktury swojego e-maila i instalacji Home Assistant.\n\nAby uzyskać szczegółowe informacje na temat opcji [integracji Mail and Packages](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), zapoznaj się z [sekcją konfiguracji, szablonów i automatyzacji](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHubie.\n\nJeśli korzystasz z przekierowanych e-maili Amazon, oddziel każdy adres przecinkiem lub wprowadź (brak), aby wyczyścić to ustawienie.",
|
||||
"title": "Poczta i paczki (krok 2 z 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Ścieżka do niestandardowego obrazu: (np.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Wprowadź ścieżkę i nazwę pliku do swojego niestandardowego obrazu bez maila poniżej.\n\nPrzykład: images/custom_nomail.gif",
|
||||
"title": "Poczta i paczki (Krok 3 z 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domena Amazon",
|
||||
"amazon_fwds": "Amazon przekazał adresy e-mail",
|
||||
"amazon_days": "Dni do sprawdzenia dla wiadomości e-mail od Amazon"
|
||||
},
|
||||
"description": "Wprowadź domenę, z której Amazon wysyła e-maile (np. amazon.com lub amazon.de)\n\nJeśli korzystasz z przekierowanych e-maili od Amazon, oddziel każdy adres przecinkiem lub wpisz (brak), aby wyczyścić to ustawienie.\n\nUwaga: jeśli w poprzednim kroku skonfigurowałeś nagłówek przekierowania, pole adresu przekierowania nie pojawi się — nagłówek automatycznie obsługuje dopasowywanie e-maili Amazon.",
|
||||
"title": "Ustawienia Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Katalog do przechowywania obrazów"
|
||||
},
|
||||
"description": "Wprowadź katalog, w którym chciałbyś przechowywać swoje obrazy.\nDomyślnie jest on automatycznie wypełniany.",
|
||||
"title": "Lokalizacja przechowywania obrazów"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Katalog do przechowywania obrazów"
|
||||
},
|
||||
"description": "Wprowadź katalog, w którym chciałbyś przechowywać swoje obrazy.\nDomyślnie jest on automatycznie wypełniany.",
|
||||
"title": "Lokalizacja przechowywania obrazów"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Nagłówek przekierowania (np. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Przekierowane adresy e-mail"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Jeśli Twoja usługa przekierowania zawiera oryginalnego nadawcę w nagłówku, wprowadź tutaj nazwę nagłówka. Ma to pierwszeństwo przed poniższą listą adresów. Wprowadź '(none)' lub pozostaw puste, aby zamiast tego użyć listy adresów.",
|
||||
"forwarded_emails": "Lista oddzielona przecinkami adresów przekierowania, z których będą przychodziły wiadomości. Pozostaw '(none)', jeśli używasz powyższego nagłówka przekierowania."
|
||||
},
|
||||
"description": "Wybierz sposób dopasowywania przekierowanych wiadomości e-mail.\n\n**Opcja 1 — Nagłówek przekierowania**: Jeśli Twoja usługa (np. SimpleLogin) zawiera oryginalnego nadawcę w niestandardowym nagłówku, wprowadź nazwę tego nagłówka. Mail and Packages użyje go do automatycznego dopasowywania adresów przewoźników.\n\n**Opcja 2 — Lista adresów**: Wprowadź adresy przekierowania przypisane przez usługę do każdego przewoźnika, oddzielone przecinkami.",
|
||||
"title": "Ustawienia przekierowanych wiadomości e-mail"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Nagłówek przekierowania (np. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Przekierowane adresy e-mail"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Jeśli Twoja usługa przekierowania zawiera oryginalnego nadawcę w nagłówku, wprowadź tutaj nazwę nagłówka. Ma to pierwszeństwo przed poniższą listą adresów. Wprowadź '(none)' lub pozostaw puste, aby zamiast tego użyć listy adresów.",
|
||||
"forwarded_emails": "Lista oddzielona przecinkami adresów przekierowania, z których będą przychodziły wiadomości. Pozostaw '(none)', jeśli używasz powyższego nagłówka przekierowania."
|
||||
},
|
||||
"description": "Wybierz sposób dopasowywania przekierowanych wiadomości e-mail.\n\n**Opcja 1 — Nagłówek przekierowania**: Jeśli Twoja usługa (np. SimpleLogin) zawiera oryginalnego nadawcę w niestandardowym nagłówku, wprowadź nazwę tego nagłówka. Mail and Packages użyje go do automatycznego dopasowywania adresów przewoźników.\n\n**Opcja 2 — Lista adresów**: Wprowadź adresy przekierowania przypisane przez usługę do każdego przewoźnika, oddzielone przecinkami.",
|
||||
"title": "Ustawienia przekierowanych wiadomości e-mail"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Proszę wprowadzić informacje o połączeniu z serwerem pocztowym.",
|
||||
"data": {
|
||||
"host": "Gospodarz",
|
||||
"password": "Hasło",
|
||||
"port": "Port",
|
||||
"username": "Nazwa Użytkownika",
|
||||
"verify_ssl": "Sprawdź certyfikat SSL",
|
||||
"imap_security": "Bezpieczeństwo IMAP"
|
||||
},
|
||||
"title": "Poczta i paczki (krok 1 z 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Gospodarz",
|
||||
"password": "Hasło",
|
||||
"port": "Port",
|
||||
"username": "Nazwa Użytkownika",
|
||||
"imap_security": "Bezpieczeństwo IMAP",
|
||||
"verify_ssl": "Sprawdź certyfikat SSL"
|
||||
},
|
||||
"description": "Proszę wprowadzić informacje o połączeniu z serwerem pocztowym.",
|
||||
"title": "Poczta i paczki (krok 1 z 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Somente uma única configuração de correio e pacotes é permitida.",
|
||||
"reconfigure_successful": "Reconfiguração bem-sucedida"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Não foi possível conectar ou fazer login no servidor de email. Por favor, verifique o log para obter detalhes.",
|
||||
"invalid_path": "Por favor, guarde as imagens em outro diretório.",
|
||||
"ffmpeg_not_found": "Gerar MP4 requer ffmpeg",
|
||||
"amazon_domain": "Endereço de encaminhamento de email inválido.",
|
||||
"file_not_found": "Arquivo de imagem não encontrado",
|
||||
"invalid_email_format": "Formato de endereço de email inválido.",
|
||||
"missing_forwarded_emails": "Endereços de email encaminhados em falta. Pode introduzir '(none)' para limpar esta configuração.",
|
||||
"path_not_found": "Diretório não encontrado",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Pasta de Correio",
|
||||
"resources": "Lista de Sensores",
|
||||
"scan_interval": "Intervalo de digitalização (minutos)",
|
||||
"image_path": "Caminho da imagem",
|
||||
"gif_duration": "Duração da imagem (segundos)",
|
||||
"image_security": "Nome da imagem aleatória",
|
||||
"imap_timeout": "Tempo em segundos antes do tempo limite de conexão (segundos, mínimo 10)",
|
||||
"generate_mp4": "Crie mp4 a partir de imagens",
|
||||
"allow_external": "Crie imagem para aplicativos de notificação",
|
||||
"custom_img": "Usar imagem personalizada USPS 'sem correio'?",
|
||||
"amazon_custom_img": "Usar imagem personalizada Amazon 'sem entrega'?",
|
||||
"ups_custom_img": "Usar imagem personalizada UPS 'sem entrega'?",
|
||||
"walmart_custom_img": "Usar imagem personalizada Walmart 'sem entrega'?",
|
||||
"fedex_custom_img": "Usar imagem personalizada FedEx 'sem entrega'?",
|
||||
"generic_custom_img": "Usar imagem personalizada genérica 'sem entrega'?",
|
||||
"generate_grid": "Criar grade de imagens para modelos de visão LLM",
|
||||
"allow_forwarded_emails": "Permitir emails encaminhados além do padrão de um serviço (ex: no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Conclua a configuração, personalizando o seguinte com base na sua estrutura de email e instalação do Home Assistant. \n\n Para obter detalhes sobre as opções [integração de Mail e Pacotes] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), revise as opções de [configuração, modelos e seção de automações] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) no GitHub.",
|
||||
"title": "Correio e pacotes (Etapa 2 de 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Caminho para imagem personalizada: (ex: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Insira o caminho e o nome do arquivo para a sua imagem personalizada de sem correio abaixo.\n\nExemplo: images/custom_nomail.gif",
|
||||
"title": "Correio e Pacotes (Etapa 3 de 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domínio da Amazon",
|
||||
"amazon_fwds": "A Amazon encaminhou endereços de email",
|
||||
"amazon_days": "Dias atrás para verificar emails da Amazon"
|
||||
},
|
||||
"description": "Por favor, insira o domínio de onde a Amazon envia e-mails (por exemplo: amazon.com ou amazon.de)\n\nSe estiver usando e-mails encaminhados pela Amazon, separe cada endereço com uma vírgula ou insira (nenhum) para limpar essa configuração.\n\nNota: se configurou um cabeçalho de reencaminhamento no passo anterior, o campo de endereço de reencaminhamento não aparecerá — o cabeçalho trata automaticamente da correspondência de e-mails da Amazon.",
|
||||
"title": "Configurações da Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Pasta de Correio",
|
||||
"scan_interval": "Intervalo de digitalização (minutos)",
|
||||
"image_path": "Caminho da imagem",
|
||||
"gif_duration": "Duração da imagem (segundos)",
|
||||
"image_security": "Nome da imagem aleatória",
|
||||
"generate_mp4": "Crie mp4 a partir de imagens",
|
||||
"resources": "Lista de Sensores",
|
||||
"imap_timeout": "Tempo em segundos antes do tempo limite de conexão (segundos, mínimo 10)",
|
||||
"allow_external": "Crie imagem para aplicativos de notificação",
|
||||
"custom_img": "Usar imagem personalizada USPS 'sem correio'?",
|
||||
"amazon_custom_img": "Usar imagem personalizada Amazon 'sem entrega'?",
|
||||
"ups_custom_img": "Usar imagem personalizada UPS 'sem entrega'?",
|
||||
"walmart_custom_img": "Usar imagem personalizada Walmart 'sem entrega'?",
|
||||
"fedex_custom_img": "Usar imagem personalizada FedEx 'sem entrega'?",
|
||||
"generic_custom_img": "Usar imagem personalizada genérica 'sem entrega'?",
|
||||
"generate_grid": "Criar grade de imagens para modelos de visão LLM",
|
||||
"allow_forwarded_emails": "Permitir emails encaminhados além do padrão de um serviço (ex: no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Conclua a configuração personalizando o seguinte com base na estrutura do seu email e na instalação do Home Assistant.\n\nPara detalhes sobre as opções de [integração de Correio e Pacotes](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), consulte a [seção de configuração, modelos e automações](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) no GitHub.\n\nSe estiver usando emails encaminhados da Amazon, separe cada endereço com uma vírgula ou insira (nenhum) para limpar essa configuração.",
|
||||
"title": "Correio e pacotes (Etapa 2 de 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Caminho para imagem personalizada: (ex.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Insira o caminho e o nome do arquivo para a sua imagem personalizada de sem correio abaixo.\n\nExemplo: images/custom_nomail.gif",
|
||||
"title": "Correio e Pacotes (Etapa 3 de 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domínio da Amazon",
|
||||
"amazon_fwds": "A Amazon encaminhou endereços de email",
|
||||
"amazon_days": "Dias atrás para verificar emails da Amazon"
|
||||
},
|
||||
"description": "Por favor, insira o domínio de onde a Amazon envia e-mails (por exemplo: amazon.com ou amazon.de)\n\nSe estiver usando e-mails encaminhados pela Amazon, separe cada endereço com uma vírgula ou insira (nenhum) para limpar essa configuração.\n\nNota: se configurou um cabeçalho de reencaminhamento no passo anterior, o campo de endereço de reencaminhamento não aparecerá — o cabeçalho trata automaticamente da correspondência de e-mails da Amazon.",
|
||||
"title": "Configurações da Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Diretório para armazenar imagens"
|
||||
},
|
||||
"description": "Por favor, insira o diretório em que você gostaria que suas imagens fossem armazenadas.\nO padrão é preenchido automaticamente.",
|
||||
"title": "Local de armazenamento de imagens"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Diretório para armazenar imagens"
|
||||
},
|
||||
"description": "Por favor, insira o diretório em que você gostaria que suas imagens fossem armazenadas.\nO padrão é preenchido automaticamente.",
|
||||
"title": "Local de armazenamento de imagens"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabeçalho de reencaminhamento (ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Endereços de email encaminhados"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Se o seu serviço de reencaminhamento inclui o remetente original num cabeçalho, introduza aqui o nome do cabeçalho. Isto tem prioridade sobre a lista de endereços abaixo. Introduza '(none)' ou deixe em branco para usar a lista de endereços.",
|
||||
"forwarded_emails": "Lista separada por vírgulas dos endereços de reencaminhamento de onde chegarão as mensagens. Deixe '(none)' se estiver a usar o cabeçalho de reencaminhamento acima."
|
||||
},
|
||||
"description": "Escolha como fazer a correspondência dos e-mails reencaminhados.\n\n**Opção 1 — Cabeçalho de reencaminhamento**: Se o seu serviço (ex. SimpleLogin) inclui o remetente original num cabeçalho personalizado, introduza esse nome de cabeçalho. O Mail and Packages irá utilizá-lo para fazer a correspondência automática dos endereços das transportadoras.\n\n**Opção 2 — Lista de endereços**: Introduza os endereços de reencaminhamento que o seu serviço atribuiu a cada transportadora, separados por vírgulas.",
|
||||
"title": "Definições de e-mails reencaminhados"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabeçalho de reencaminhamento (ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Endereços de email encaminhados"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Se o seu serviço de reencaminhamento inclui o remetente original num cabeçalho, introduza aqui o nome do cabeçalho. Isto tem prioridade sobre a lista de endereços abaixo. Introduza '(none)' ou deixe em branco para usar a lista de endereços.",
|
||||
"forwarded_emails": "Lista separada por vírgulas dos endereços de reencaminhamento de onde chegarão as mensagens. Deixe '(none)' se estiver a usar o cabeçalho de reencaminhamento acima."
|
||||
},
|
||||
"description": "Escolha como fazer a correspondência dos e-mails reencaminhados.\n\n**Opção 1 — Cabeçalho de reencaminhamento**: Se o seu serviço (ex. SimpleLogin) inclui o remetente original num cabeçalho personalizado, introduza esse nome de cabeçalho. O Mail and Packages irá utilizá-lo para fazer a correspondência automática dos endereços das transportadoras.\n\n**Opção 2 — Lista de endereços**: Introduza os endereços de reencaminhamento que o seu serviço atribuiu a cada transportadora, separados por vírgulas.",
|
||||
"title": "Definições de e-mails reencaminhados"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Por favor, insira as informações de conexão do seu servidor de email.",
|
||||
"data": {
|
||||
"host": "Hospedeiro",
|
||||
"password": "Senha",
|
||||
"port": "Porta",
|
||||
"username": "Nome do usuário",
|
||||
"verify_ssl": "Verificar Certificado SSL",
|
||||
"imap_security": "Segurança IMAP"
|
||||
},
|
||||
"title": "Correio e pacotes (Etapa 1 de 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Hospedeiro",
|
||||
"password": "Senha",
|
||||
"port": "Porta",
|
||||
"username": "Nome do usuário",
|
||||
"imap_security": "Segurança IMAP",
|
||||
"verify_ssl": "Verificar Certificado SSL"
|
||||
},
|
||||
"description": "Por favor, insira as informações de conexão do seu servidor de email.",
|
||||
"title": "Correio e pacotes (Etapa 1 de 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Somente uma única configuração de correio e pacotes é permitida.",
|
||||
"reconfigure_successful": "Reconfiguração bem-sucedida"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Não foi possível conectar ou fazer login no servidor de email. Por favor, verifique o log para obter detalhes.",
|
||||
"invalid_path": "Por favor, guarde as imagens em outro diretório.",
|
||||
"ffmpeg_not_found": "Gerar MP4 requer ffmpeg",
|
||||
"amazon_domain": "Endereço de encaminhamento de email inválido.",
|
||||
"file_not_found": "Arquivo de imagem não encontrado",
|
||||
"invalid_email_format": "Formato de endereço de email inválido.",
|
||||
"missing_forwarded_emails": "Endereços de email encaminhados em falta. Você pode inserir '(none)' para limpar esta configuração.",
|
||||
"path_not_found": "Diretório não encontrado",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Pasta de Correio",
|
||||
"resources": "Lista de Sensores",
|
||||
"scan_interval": "Intervalo de digitalização (minutos)",
|
||||
"image_path": "Caminho da imagem",
|
||||
"gif_duration": "Duração da imagem (segundos)",
|
||||
"image_security": "Nome da imagem aleatória",
|
||||
"imap_timeout": "Tempo em segundos antes do tempo limite de conexão (segundos, mínimo 10)",
|
||||
"generate_mp4": "Crie mp4 a partir de imagens",
|
||||
"allow_external": "Crie imagem para aplicativos de notificação",
|
||||
"custom_img": "Usar imagem personalizada USPS 'sem correio'?",
|
||||
"amazon_custom_img": "Usar imagem personalizada Amazon 'sem entrega'?",
|
||||
"ups_custom_img": "Usar imagem personalizada UPS 'sem entrega'?",
|
||||
"walmart_custom_img": "Usar imagem personalizada Walmart 'sem entrega'?",
|
||||
"fedex_custom_img": "Usar imagem personalizada FedEx 'sem entrega'?",
|
||||
"generic_custom_img": "Usar imagem personalizada genérica 'sem entrega'?",
|
||||
"generate_grid": "Criar grade de imagens para modelos de visão LLM",
|
||||
"allow_forwarded_emails": "Permitir e-mails encaminhados além do padrão de um serviço (ex: no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Conclua a configuração, personalizando o seguinte com base na sua estrutura de email e instalação do Home Assistant. \n\n Para obter detalhes sobre as opções [integração de Mail e Pacotes] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), revise as opções de [configuração, modelos e seção de automações] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) no GitHub.",
|
||||
"title": "Correio e pacotes (Etapa 2 de 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Caminho para imagem personalizada: (ex: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Insira o caminho e o nome do arquivo para a sua imagem personalizada de sem correio abaixo.\n\nExemplo: images/custom_nomail.gif",
|
||||
"title": "Correio e Pacotes (Etapa 3 de 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domínio da Amazon",
|
||||
"amazon_fwds": "A Amazon encaminhou endereços de email",
|
||||
"amazon_days": "Dias atrás para verificar os emails da Amazon"
|
||||
},
|
||||
"description": "Por favor, insira o domínio do qual a Amazon envia e-mails (por exemplo: amazon.com ou amazon.de)\n\nSe estiver usando e-mails encaminhados pela Amazon, separe cada endereço com uma vírgula ou insira (nenhum) para limpar essa configuração.\n\nNota: se você configurou um cabeçalho de encaminhamento na etapa anterior, o campo de endereço de encaminhamento não aparecerá — o cabeçalho trata automaticamente da correspondência de e-mails da Amazon.",
|
||||
"title": "Configurações da Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Pasta de Correio",
|
||||
"scan_interval": "Intervalo de digitalização (minutos)",
|
||||
"image_path": "Caminho da imagem",
|
||||
"gif_duration": "Duração da imagem (segundos)",
|
||||
"image_security": "Nome da imagem aleatória",
|
||||
"generate_mp4": "Crie mp4 a partir de imagens",
|
||||
"resources": "Lista de Sensores",
|
||||
"imap_timeout": "Tempo em segundos antes do tempo limite de conexão (segundos, mínimo 10)",
|
||||
"allow_external": "Crie imagem para aplicativos de notificação",
|
||||
"custom_img": "Usar imagem personalizada USPS 'sem correio'?",
|
||||
"amazon_custom_img": "Usar imagem personalizada Amazon 'sem entrega'?",
|
||||
"ups_custom_img": "Usar imagem personalizada UPS 'sem entrega'?",
|
||||
"walmart_custom_img": "Usar imagem personalizada Walmart 'sem entrega'?",
|
||||
"fedex_custom_img": "Usar imagem personalizada FedEx 'sem entrega'?",
|
||||
"generic_custom_img": "Usar imagem personalizada genérica 'sem entrega'?",
|
||||
"generate_grid": "Criar grade de imagens para modelos de visão LLM",
|
||||
"allow_forwarded_emails": "Permitir e-mails encaminhados além do padrão de um serviço (ex: no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Conclua a configuração personalizando o seguinte com base na estrutura do seu email e na instalação do Home Assistant.\n\nPara detalhes sobre as opções de [integração de Mail e Pacotes](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration), consulte a [seção de configuração, modelos e automações](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) no GitHub.\n\nSe estiver usando emails encaminhados da Amazon, separe cada endereço com uma vírgula ou insira (nenhum) para limpar essa configuração.",
|
||||
"title": "Correio e pacotes (Etapa 2 de 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Caminho para imagem personalizada: (ex.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Insira o caminho e o nome do arquivo para a sua imagem personalizada de sem correio abaixo.\n\nExemplo: images/custom_nomail.gif",
|
||||
"title": "Correio e Pacotes (Etapa 3 de 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Domínio da Amazon",
|
||||
"amazon_fwds": "A Amazon encaminhou endereços de email",
|
||||
"amazon_days": "Dias atrás para verificar os emails da Amazon"
|
||||
},
|
||||
"description": "Por favor, insira o domínio do qual a Amazon envia e-mails (por exemplo: amazon.com ou amazon.de)\n\nSe estiver usando e-mails encaminhados pela Amazon, separe cada endereço com uma vírgula ou insira (nenhum) para limpar essa configuração.\n\nNota: se você configurou um cabeçalho de encaminhamento na etapa anterior, o campo de endereço de encaminhamento não aparecerá — o cabeçalho trata automaticamente da correspondência de e-mails da Amazon.",
|
||||
"title": "Configurações da Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Diretório para armazenar imagens"
|
||||
},
|
||||
"description": "Por favor, insira o diretório em que você gostaria que suas imagens fossem armazenadas.\nO padrão é preenchido automaticamente.",
|
||||
"title": "Local de armazenamento de imagens"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Diretório para armazenar imagens"
|
||||
},
|
||||
"description": "Por favor, insira o diretório em que você gostaria que suas imagens fossem armazenadas.\nO padrão é preenchido automaticamente.",
|
||||
"title": "Local de armazenamento de imagens"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabeçalho de encaminhamento (ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Endereços de e-mail encaminhados"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Se o seu serviço de encaminhamento inclui o remetente original em um cabeçalho, insira o nome do cabeçalho aqui. Isso tem precedência sobre a lista de endereços abaixo. Insira '(none)' ou deixe em branco para usar a lista de endereços.",
|
||||
"forwarded_emails": "Lista separada por vírgulas dos endereços de encaminhamento dos quais as mensagens chegarão. Deixe '(none)' se estiver usando o cabeçalho de encaminhamento acima."
|
||||
},
|
||||
"description": "Escolha como corresponder os e-mails encaminhados.\n\n**Opção 1 — Cabeçalho de encaminhamento**: Se o seu serviço (ex. SimpleLogin) inclui o remetente original em um cabeçalho personalizado, insira esse nome de cabeçalho. O Mail and Packages o usará para corresponder os endereços das transportadoras automaticamente.\n\n**Opção 2 — Lista de endereços**: Insira os endereços de encaminhamento que seu serviço atribuiu a cada transportadora, separados por vírgulas.",
|
||||
"title": "Configurações de e-mails encaminhados"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Cabeçalho de encaminhamento (ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Endereços de e-mail encaminhados"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Se o seu serviço de encaminhamento inclui o remetente original em um cabeçalho, insira o nome do cabeçalho aqui. Isso tem precedência sobre a lista de endereços abaixo. Insira '(none)' ou deixe em branco para usar a lista de endereços.",
|
||||
"forwarded_emails": "Lista separada por vírgulas dos endereços de encaminhamento dos quais as mensagens chegarão. Deixe '(none)' se estiver usando o cabeçalho de encaminhamento acima."
|
||||
},
|
||||
"description": "Escolha como corresponder os e-mails encaminhados.\n\n**Opção 1 — Cabeçalho de encaminhamento**: Se o seu serviço (ex. SimpleLogin) inclui o remetente original em um cabeçalho personalizado, insira esse nome de cabeçalho. O Mail and Packages o usará para corresponder os endereços das transportadoras automaticamente.\n\n**Opção 2 — Lista de endereços**: Insira os endereços de encaminhamento que seu serviço atribuiu a cada transportadora, separados por vírgulas.",
|
||||
"title": "Configurações de e-mails encaminhados"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Por favor, insira as informações de conexão do seu servidor de e-mail.",
|
||||
"data": {
|
||||
"host": "Hospedeiro",
|
||||
"password": "Senha",
|
||||
"port": "Porta",
|
||||
"username": "Nome do usuário",
|
||||
"verify_ssl": "Verificar Certificado SSL",
|
||||
"imap_security": "Segurança IMAP"
|
||||
},
|
||||
"title": "Correio e pacotes (Etapa 1 de 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Hospedeiro",
|
||||
"password": "Senha",
|
||||
"port": "Porta",
|
||||
"username": "Nome do usuário",
|
||||
"imap_security": "Segurança IMAP",
|
||||
"verify_ssl": "Verificar Certificado SSL"
|
||||
},
|
||||
"description": "Por favor, insira as informações de conexão do seu servidor de e-mail.",
|
||||
"title": "Correio e pacotes (Etapa 1 de 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Разрешена только одна конфигурация Почты и Пакетов.",
|
||||
"reconfigure_successful": "Перенастройка успешно завершена"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Невозможно подключиться или войти на почтовый сервер. Пожалуйста, проверьте журнал для деталей.",
|
||||
"invalid_path": "Пожалуйста, храните изображения в другом каталоге.",
|
||||
"ffmpeg_not_found": "Для создания MP4 требуется ffmpeg",
|
||||
"amazon_domain": "Недействительный адрес электронной почты для переадресации.",
|
||||
"file_not_found": "Файл изображения не найден",
|
||||
"invalid_email_format": "Неверный формат адреса электронной почты.",
|
||||
"missing_forwarded_emails": "Отсутствуют переадресованные адреса электронной почты. Вы можете ввести '(none)', чтобы очистить этот параметр.",
|
||||
"path_not_found": "Директория не найдена",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Папка почты",
|
||||
"resources": "Список датчиков",
|
||||
"scan_interval": "Интервал сканирования (минуты)",
|
||||
"image_path": "Путь к изображению",
|
||||
"gif_duration": "Длительность изображения (секунды)",
|
||||
"image_security": "Случайное изображение Имя файла",
|
||||
"imap_timeout": "Время в секундах до тайм-аута соединения (секунды, минимум 10)",
|
||||
"generate_mp4": "Создать mp4 из изображений",
|
||||
"allow_external": "Создать изображение для приложений уведомлений",
|
||||
"custom_img": "Использовать пользовательское изображение USPS 'нет почты'?",
|
||||
"amazon_custom_img": "Использовать пользовательское изображение Amazon 'нет доставки'?",
|
||||
"ups_custom_img": "Использовать пользовательское изображение UPS 'нет доставки'?",
|
||||
"walmart_custom_img": "Использовать пользовательское изображение Walmart 'нет доставки'?",
|
||||
"fedex_custom_img": "Использовать пользовательское изображение FedEx 'нет доставки'?",
|
||||
"generic_custom_img": "Использовать пользовательское изображение общее 'нет доставки'?",
|
||||
"generate_grid": "Создать сетку изображений для моделей зрения LLM",
|
||||
"allow_forwarded_emails": "Разрешить переадресованные электронные письма в дополнение к значениям по умолчанию службы (например, no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Завершите настройку, настроив следующие параметры в зависимости от структуры электронной почты и установки Home Assistant. \n\n Подробнее о параметрах [Интеграция с почтой и пакетами] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) см. В разделе [конфигурация, шаблоны и раздел автоматизации] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) на GitHub.",
|
||||
"title": "Почта и пакеты (шаг 2 из 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Путь к пользовательскому изображению: (например: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Введите путь и имя файла к вашему пользовательскому изображению без почты ниже.\n\nПример: images/custom_nomail.gif",
|
||||
"title": "Почта и посылки (Шаг 3 из 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Домен Amazon",
|
||||
"amazon_fwds": "Amazon переслал адреса электронной почты",
|
||||
"amazon_days": "Дни для проверки электронных писем от Amazon"
|
||||
},
|
||||
"description": "Пожалуйста, введите домен, с которого Amazon отправляет электронные письма (например: amazon.com или amazon.de)\n\nЕсли вы используете переадресованные электронные письма от Amazon, разделите каждый адрес запятой или введите (none), чтобы очистить эту настройку.\n\nПримечание: если вы настроили заголовок переадресации на предыдущем шаге, поле адреса переадресации не появится — заголовок автоматически обрабатывает сопоставление писем Amazon.",
|
||||
"title": "Настройки Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Папка почты",
|
||||
"scan_interval": "Интервал сканирования (минуты)",
|
||||
"image_path": "Путь к изображению",
|
||||
"gif_duration": "Длительность изображения (секунды)",
|
||||
"image_security": "Случайное изображение Имя файла",
|
||||
"generate_mp4": "Создать mp4 из изображений",
|
||||
"resources": "Список датчиков",
|
||||
"imap_timeout": "Время в секундах до тайм-аута соединения (секунды, минимум 10)",
|
||||
"allow_external": "Создать изображение для приложений уведомлений",
|
||||
"custom_img": "Использовать пользовательское изображение USPS 'нет почты'?",
|
||||
"amazon_custom_img": "Использовать пользовательское изображение Amazon 'нет доставки'?",
|
||||
"ups_custom_img": "Использовать пользовательское изображение UPS 'нет доставки'?",
|
||||
"walmart_custom_img": "Использовать пользовательское изображение Walmart 'нет доставки'?",
|
||||
"fedex_custom_img": "Использовать пользовательское изображение FedEx 'нет доставки'?",
|
||||
"generic_custom_img": "Использовать пользовательское изображение общее 'нет доставки'?",
|
||||
"generate_grid": "Создать сетку изображений для моделей зрения LLM",
|
||||
"allow_forwarded_emails": "Разрешить переадресованные электронные письма в дополнение к значениям по умолчанию службы (например, no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Завершите настройку, настроив следующее в соответствии со структурой вашей электронной почты и установкой Home Assistant.\n\nДля получения подробной информации о вариантах [интеграции Почта и Пакеты](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) ознакомьтесь с [разделом конфигурации, шаблонов и автоматизации](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) на GitHub.\n\nЕсли вы используете переадресованные электронные письма Amazon, разделите каждый адрес запятой или введите (none), чтобы очистить эту настройку.",
|
||||
"title": "Почта и пакеты (шаг 2 из 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Путь к пользовательскому изображению: (например: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Введите путь и имя файла к вашему пользовательскому изображению без почты ниже.\n\nПример: images/custom_nomail.gif",
|
||||
"title": "Почта и посылки (Шаг 3 из 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Домен Amazon",
|
||||
"amazon_fwds": "Amazon переслал адреса электронной почты",
|
||||
"amazon_days": "Дни для проверки электронных писем от Amazon"
|
||||
},
|
||||
"description": "Пожалуйста, введите домен, с которого Amazon отправляет электронные письма (например: amazon.com или amazon.de)\n\nЕсли вы используете переадресованные электронные письма от Amazon, разделите каждый адрес запятой или введите (none), чтобы очистить эту настройку.\n\nПримечание: если вы настроили заголовок переадресации на предыдущем шаге, поле адреса переадресации не появится — заголовок автоматически обрабатывает сопоставление писем Amazon.",
|
||||
"title": "Настройки Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Каталог для хранения изображений"
|
||||
},
|
||||
"description": "Пожалуйста, введите директорию, в которой вы хотите хранить свои изображения. \nПо умолчанию она заполняется автоматически.",
|
||||
"title": "Место хранения изображения"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Каталог для хранения изображений"
|
||||
},
|
||||
"description": "Пожалуйста, введите директорию, в которой вы хотите хранить свои изображения. \nПо умолчанию она заполняется автоматически.",
|
||||
"title": "Место хранения изображения"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Заголовок переадресации (например, X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Переадресованные адреса электронной почты"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Если ваш сервис переадресации включает оригинального отправителя в заголовок, введите здесь название заголовка. Это имеет приоритет над списком адресов ниже. Введите '(none)' или оставьте пустым, чтобы использовать список адресов.",
|
||||
"forwarded_emails": "Список адресов переадресации, разделённых запятыми, с которых будут приходить сообщения. Оставьте '(none)', если используете заголовок переадресации выше."
|
||||
},
|
||||
"description": "Выберите способ сопоставления переадресованных писем.\n\n**Вариант 1 — Заголовок переадресации**: Если ваш сервис (например, SimpleLogin) включает оригинального отправителя в пользовательский заголовок, введите это название заголовка. Mail and Packages будет использовать его для автоматического сопоставления адресов перевозчиков.\n\n**Вариант 2 — Список адресов**: Введите адреса переадресации, которые ваш сервис назначил каждому перевозчику, через запятую.",
|
||||
"title": "Настройки переадресованных писем"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Заголовок переадресации (например, X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Переадресованные адреса электронной почты"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Если ваш сервис переадресации включает оригинального отправителя в заголовок, введите здесь название заголовка. Это имеет приоритет над списком адресов ниже. Введите '(none)' или оставьте пустым, чтобы использовать список адресов.",
|
||||
"forwarded_emails": "Список адресов переадресации, разделённых запятыми, с которых будут приходить сообщения. Оставьте '(none)', если используете заголовок переадресации выше."
|
||||
},
|
||||
"description": "Выберите способ сопоставления переадресованных писем.\n\n**Вариант 1 — Заголовок переадресации**: Если ваш сервис (например, SimpleLogin) включает оригинального отправителя в пользовательский заголовок, введите это название заголовка. Mail and Packages будет использовать его для автоматического сопоставления адресов перевозчиков.\n\n**Вариант 2 — Список адресов**: Введите адреса переадресации, которые ваш сервис назначил каждому перевозчику, через запятую.",
|
||||
"title": "Настройки переадресованных писем"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Пожалуйста, введите информацию о подключении к вашему почтовому серверу.",
|
||||
"data": {
|
||||
"host": "хозяин",
|
||||
"password": "пароль",
|
||||
"port": "порт",
|
||||
"username": "имя пользователя",
|
||||
"verify_ssl": "Проверить SSL-сертификат",
|
||||
"imap_security": "Безопасность IMAP"
|
||||
},
|
||||
"title": "Почта и Пакеты (Шаг 1 из 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "хозяин",
|
||||
"password": "пароль",
|
||||
"port": "порт",
|
||||
"username": "имя пользователя",
|
||||
"imap_security": "Безопасность IMAP",
|
||||
"verify_ssl": "Проверить SSL-сертификат"
|
||||
},
|
||||
"description": "Пожалуйста, введите информацию о подключении к вашему почтовому серверу.",
|
||||
"title": "Почта и Пакеты (Шаг 1 из 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Je povolená iba jedna konfigurácia pošty a balíkov.",
|
||||
"reconfigure_successful": "Rekonfigurácia úspešná"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nedá sa pripojiť alebo prihlásiť na poštový server. Podrobnosti nájdete v protokole.",
|
||||
"invalid_path": "Uložte obrázky do iného adresára.",
|
||||
"ffmpeg_not_found": "Vytvorenie MP4 vyžaduje ffmpeg",
|
||||
"amazon_domain": "Neplatná adresa pre presmerovanie e-mailov.",
|
||||
"file_not_found": "Obrázokový súbor nebol nájdený",
|
||||
"invalid_email_format": "Neplatný formát e-mailovej adresy.",
|
||||
"missing_forwarded_emails": "Chýbajú preposielané e-mailové adresy. Môžete zadať '(none)' na vymazanie tohto nastavenia.",
|
||||
"path_not_found": "Adresár nebol nájdený",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Priečinok pošty",
|
||||
"resources": "Zoznam senzorov",
|
||||
"scan_interval": "Interval skenovania (minúty)",
|
||||
"image_path": "Cesta obrázka",
|
||||
"gif_duration": "Trvanie obrázka (sekundy)",
|
||||
"image_security": "Náhodný názov súboru obrázka",
|
||||
"imap_timeout": "Čas v sekundách pred časovým limitom pripojenia (sekundy, minimálne 10)",
|
||||
"generate_mp4": "Vytvorte mp4 z obrázkov",
|
||||
"allow_external": "Vytvorte obrázok pre aplikácie upozornení",
|
||||
"custom_img": "Použiť vlastný obrázok USPS 'bez pošty'?",
|
||||
"amazon_custom_img": "Použiť vlastný obrázok Amazon 'bez dodávky'?",
|
||||
"ups_custom_img": "Použiť vlastný obrázok UPS 'bez dodávky'?",
|
||||
"walmart_custom_img": "Použiť vlastný obrázok Walmart 'bez dodávky'?",
|
||||
"fedex_custom_img": "Použiť vlastný obrázok FedEx 'bez dodávky'?",
|
||||
"generic_custom_img": "Použiť vlastný obrázok všeobecný 'bez dodávky'?",
|
||||
"generate_grid": "Vytvoriť mriežku obrázkov pre modely videnia LLM",
|
||||
"allow_forwarded_emails": "Povoliť preposielané e-maily okrem predvolenej hodnoty služby (napr. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Dokončite konfiguráciu prispôsobením nasledujúcich položiek na základe štruktúry e-mailu a inštalácie Home Assistant.\n\nPodrobnosti nájdete na [Pošta a balíky integrácia](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) možnosti si pozrite v časti [konfigurácia, šablóny a automatizácie](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHube.",
|
||||
"title": "Pošta a balíky (krok 2 z 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Cesta k vlastnému obrázku: (napr.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Zadajte cestu a názov súboru k vášmu vlastnému obrázku bez pošty nižšie.\n\nPríklad: images/custom_nomail.gif",
|
||||
"title": "Pošta a balíky (Krok 3 z 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Doména Amazon",
|
||||
"amazon_fwds": "Amazon preposlal emailové adresy",
|
||||
"amazon_days": "Dni späť na kontrolu e-mailov od Amazonu"
|
||||
},
|
||||
"description": "Prosím, zadajte doménu, z ktorej Amazon posiela e-maily (napríklad: amazon.com alebo amazon.de)\n\nAk používate preposlané e-maily od Amazonu, oddelte každú adresu čiarkou alebo zadajte (žiadne) na vymazanie tohto nastavenia.\n\nPoznámka: ak ste v predchádzajúcom kroku nakonfigurovali hlavičku presmerovania, pole s adresou presmerovania sa nezobrazí — hlavička automaticky spracúva zhodu e-mailov Amazon.",
|
||||
"title": "Nastavenia Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Priečinok pošty",
|
||||
"scan_interval": "Interval skenovania (minúty)",
|
||||
"image_path": "Cesta obrázka",
|
||||
"gif_duration": "Trvanie obrázka (sekundy)",
|
||||
"image_security": "Náhodný názov súboru obrázka",
|
||||
"generate_mp4": "Vytvorte mp4 z obrázkov",
|
||||
"resources": "Zoznam senzorov",
|
||||
"imap_timeout": "Čas v sekundách pred časovým limitom pripojenia (sekundy, minimálne 10)",
|
||||
"allow_external": "Vytvorte obrázok pre aplikácie upozornení",
|
||||
"custom_img": "Použiť vlastný obrázok USPS 'bez pošty'?",
|
||||
"amazon_custom_img": "Použiť vlastný obrázok Amazon 'bez dodávky'?",
|
||||
"ups_custom_img": "Použiť vlastný obrázok UPS 'bez dodávky'?",
|
||||
"walmart_custom_img": "Použiť vlastný obrázok Walmart 'bez dodávky'?",
|
||||
"fedex_custom_img": "Použiť vlastný obrázok FedEx 'bez dodávky'?",
|
||||
"generic_custom_img": "Použiť vlastný obrázok všeobecný 'bez dodávky'?",
|
||||
"generate_grid": "Vytvoriť mriežku obrázkov pre modely videnia LLM",
|
||||
"allow_forwarded_emails": "Povoliť preposielané e-maily okrem predvolenej hodnoty služby (napr. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Dokončite konfiguráciu prispôsobením nasledujúceho na základe štruktúry vášho e-mailu a inštalácie Home Assistant.\n\nPre podrobnosti o možnostiach [integrácie Mail a Packages](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) si prečítajte [sekciu o konfigurácii, šablónach a automatizáciách](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHub.\n\nAk používate preposlané e-maily od Amazonu, oddelte každú adresu čiarkou alebo zadajte (none) na vymazanie tohto nastavenia.",
|
||||
"title": "Pošta a balíky (krok 2 z 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Cesta k vlastnému obrázku: (napr.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Zadajte cestu a názov súboru k vášmu vlastnému obrázku bez pošty nižšie.\n\nPríklad: images/custom_nomail.gif",
|
||||
"title": "Pošta a balíky (Krok 3 z 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Doména Amazon",
|
||||
"amazon_fwds": "Amazon preposlal emailové adresy",
|
||||
"amazon_days": "Dni späť na kontrolu e-mailov od Amazonu"
|
||||
},
|
||||
"description": "Prosím, zadajte doménu, z ktorej Amazon posiela e-maily (napríklad: amazon.com alebo amazon.de)\n\nAk používate preposlané e-maily od Amazonu, oddelte každú adresu čiarkou alebo zadajte (žiadne) na vymazanie tohto nastavenia.\n\nPoznámka: ak ste v predchádzajúcom kroku nakonfigurovali hlavičku presmerovania, pole s adresou presmerovania sa nezobrazí — hlavička automaticky spracúva zhodu e-mailov Amazon.",
|
||||
"title": "Nastavenia Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Adresár na ukladanie obrázkov"
|
||||
},
|
||||
"description": "Prosím, zadajte adresár, v ktorom chcete ukladať svoje obrázky.\nPredvolená hodnota je automaticky vyplnená.",
|
||||
"title": "Miesto ukladania obrázkov"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Adresár na ukladanie obrázkov"
|
||||
},
|
||||
"description": "Prosím, zadajte adresár, v ktorom chcete ukladať svoje obrázky.\nPredvolená hodnota je automaticky vyplnená.",
|
||||
"title": "Miesto ukladania obrázkov"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Hlavička presmerovania (napr. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Preposielané e-mailové adresy"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Ak vaša služba presmerovania zahŕňa pôvodného odosielateľa v hlavičke, zadajte tu názov hlavičky. Toto má prednosť pred zoznamom adries nižšie. Zadajte '(none)' alebo nechajte prázdne, aby ste namiesto toho použili zoznam adries.",
|
||||
"forwarded_emails": "Zoznam presmerovacích adries oddelených čiarkami, z ktorých budú prichádzať správy. Ponechajte '(none)', ak používate vyššie uvedenú hlavičku presmerovania."
|
||||
},
|
||||
"description": "Vyberte spôsob porovnávania presmerovaných e-mailov.\n\n**Možnosť 1 — Hlavička presmerovania**: Ak vaša služba (napr. SimpleLogin) zahŕňa pôvodného odosielateľa do vlastnej hlavičky, zadajte názov tej hlavičky. Mail and Packages ho použije na automatické priradenie adries dopravcov.\n\n**Možnosť 2 — Zoznam adries**: Zadajte presmerovavacie adresy, ktoré vaša služba priradila každému dopravcovi, oddelené čiarkami.",
|
||||
"title": "Nastavenia presmerovaných e-mailov"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Hlavička presmerovania (napr. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Preposielané e-mailové adresy"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Ak vaša služba presmerovania zahŕňa pôvodného odosielateľa v hlavičke, zadajte tu názov hlavičky. Toto má prednosť pred zoznamom adries nižšie. Zadajte '(none)' alebo nechajte prázdne, aby ste namiesto toho použili zoznam adries.",
|
||||
"forwarded_emails": "Zoznam presmerovacích adries oddelených čiarkami, z ktorých budú prichádzať správy. Ponechajte '(none)', ak používate vyššie uvedenú hlavičku presmerovania."
|
||||
},
|
||||
"description": "Vyberte spôsob porovnávania presmerovaných e-mailov.\n\n**Možnosť 1 — Hlavička presmerovania**: Ak vaša služba (napr. SimpleLogin) zahŕňa pôvodného odosielateľa do vlastnej hlavičky, zadajte názov tej hlavičky. Mail and Packages ho použije na automatické priradenie adries dopravcov.\n\n**Možnosť 2 — Zoznam adries**: Zadajte presmerovavacie adresy, ktoré vaša služba priradila každému dopravcovi, oddelené čiarkami.",
|
||||
"title": "Nastavenia presmerovaných e-mailov"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Prosím, zadajte informácie o pripojení k vášmu mailovému serveru.",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Heslo",
|
||||
"port": "Port",
|
||||
"username": "Užívateľské meno",
|
||||
"verify_ssl": "Overiť SSL certifikát",
|
||||
"imap_security": "Bezpečnosť IMAP"
|
||||
},
|
||||
"title": "Pošta a balíky (1. krok z 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Heslo",
|
||||
"port": "Port",
|
||||
"username": "Užívateľské meno",
|
||||
"imap_security": "Bezpečnosť IMAP",
|
||||
"verify_ssl": "Overiť SSL certifikát"
|
||||
},
|
||||
"description": "Prosím, zadajte informácie o pripojení k vášmu mailovému serveru.",
|
||||
"title": "Pošta a balíky (1. krok z 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Dovoljena je samo ena konfiguracija pošte in paketov.",
|
||||
"reconfigure_successful": "Prekonfiguracija uspešna"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Poštnega strežnika ni mogoče povezati ali prijaviti. Za podrobnosti preverite dnevnik.",
|
||||
"invalid_path": "Shranite slike v drugem imeniku.",
|
||||
"ffmpeg_not_found": "Ustvarjanje MP4 zahteva ffmpeg",
|
||||
"amazon_domain": "Neveljaven naslov za preusmerjanje e-pošte.",
|
||||
"file_not_found": "Slikovna datoteka ni najdena",
|
||||
"invalid_email_format": "Neveljaven format e-poštnega naslova.",
|
||||
"missing_forwarded_emails": "Manjkajo posredovani e-poštni naslovi. Vnesete lahko '(none)' za izbris te nastavitve.",
|
||||
"path_not_found": "Mapa ni najdena",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "Mapa mape",
|
||||
"resources": "Seznam senzorjev",
|
||||
"scan_interval": "Interval optičnega branja (minute)",
|
||||
"image_path": "Pot slike",
|
||||
"gif_duration": "Trajanje slike (sekunde)",
|
||||
"image_security": "Naključno ime datoteke",
|
||||
"imap_timeout": "Čas v sekundah pred prekinitvijo povezave (sekunde, najmanj 10)",
|
||||
"generate_mp4": "Ustvari mp4 iz slik",
|
||||
"allow_external": "Ustvari sliko za aplikacije za obvestila",
|
||||
"custom_img": "Uporabite prilagojeno sliko USPS 'brez pošte'?",
|
||||
"amazon_custom_img": "Uporabite prilagojeno sliko Amazon 'brez dostave'?",
|
||||
"ups_custom_img": "Uporabite prilagojeno sliko UPS 'brez dostave'?",
|
||||
"walmart_custom_img": "Uporabite prilagojeno sliko Walmart 'brez dostave'?",
|
||||
"fedex_custom_img": "Uporabite prilagojeno sliko FedEx 'brez dostave'?",
|
||||
"generic_custom_img": "Uporabite prilagojeno splošno sliko 'brez dostave'?",
|
||||
"generate_grid": "Ustvari mrežo slik za vizualne modele LLM",
|
||||
"allow_forwarded_emails": "Dovoli posredovane e-poštne sporočila poleg privzete vrednosti storitve (npr. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Končajte konfiguracijo s prilagoditvijo naslednjih na podlagi strukture e-pošte in namestitve Home Assistant. \n\n Za podrobnosti o možnostih [Integracija pošte in paketov] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) preglejte [konfiguracijo, predloge in oddelku za avtomatizacije] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHubu.",
|
||||
"title": "Pošta in paketi (2. korak od 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Pot do prilagojene slike: (npr.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "V spodnje polje vnesite pot in ime datoteke do vaše prilagojene slike brez pošte.\n\nPrimer: images/custom_nomail.gif",
|
||||
"title": "Pošta in paketi (Korak 3 od 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domena",
|
||||
"amazon_fwds": "Amazon je posredoval e-poštne naslove",
|
||||
"amazon_days": "Dnevi nazaj za preverjanje Amazonovih e-poštnih sporočil"
|
||||
},
|
||||
"description": "Prosimo, vnesite domeno, s katere Amazon pošilja e-pošto (npr.: amazon.com ali amazon.de)\n\nČe uporabljate preusmerjena e-poštna sporočila Amazon, ločite vsak naslov z vejico ali vnesite (noben) za izbris te nastavitve.\n\nOpomba: če ste v prejšnjem koraku konfigurirali glavo posredovanja, polje z naslovom posredovanja ne bo prikazano — glava samodejno obravnava ujemanje Amazonovih e-poštnih sporočil.",
|
||||
"title": "Nastavitve Amazon"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "Mapa mape",
|
||||
"scan_interval": "Interval optičnega branja (minute)",
|
||||
"image_path": "Pot slike",
|
||||
"gif_duration": "Trajanje slike (sekunde)",
|
||||
"image_security": "Naključno ime datoteke",
|
||||
"generate_mp4": "Ustvari mp4 iz slik",
|
||||
"resources": "Seznam senzorjev",
|
||||
"imap_timeout": "Čas v sekundah pred prekinitvijo povezave (sekunde, najmanj 10)",
|
||||
"allow_external": "Ustvari sliko za aplikacije za obvestila",
|
||||
"custom_img": "Uporabite prilagojeno sliko USPS 'brez pošte'?",
|
||||
"amazon_custom_img": "Uporabite prilagojeno sliko Amazon 'brez dostave'?",
|
||||
"ups_custom_img": "Uporabite prilagojeno sliko UPS 'brez dostave'?",
|
||||
"walmart_custom_img": "Uporabite prilagojeno sliko Walmart 'brez dostave'?",
|
||||
"fedex_custom_img": "Uporabite prilagojeno sliko FedEx 'brez dostave'?",
|
||||
"generic_custom_img": "Uporabite prilagojeno splošno sliko 'brez dostave'?",
|
||||
"generate_grid": "Ustvari mrežo slik za vizualne modele LLM",
|
||||
"allow_forwarded_emails": "Dovoli posredovane e-poštne sporočila poleg privzete vrednosti storitve (npr. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Konfiguracijo dokončajte z prilagajanjem naslednjega glede na strukturo vašega e-poštnega sporočila in namestitev Home Assistant.\n\nZa podrobnosti o možnostih [integracije Mail and Packages](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) si oglejte [razdelek o konfiguraciji, predlogah in avtomatizacijah](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) na GitHubu.\n\nČe uporabljate preusmerjena e-poštna sporočila Amazon, ločite vsak naslov z vejico ali vnesite (noben) za izbris te nastavitve.",
|
||||
"title": "Pošta in paketi (2. korak od 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Pot do prilagojene slike: (npr.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "V spodnje polje vnesite pot in ime datoteke do vaše prilagojene slike brez pošte.\n\nPrimer: images/custom_nomail.gif",
|
||||
"title": "Pošta in paketi (korak 3 od 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domena",
|
||||
"amazon_fwds": "Amazon je posredoval e-poštne naslove",
|
||||
"amazon_days": "Dnevi nazaj za preverjanje Amazonovih e-poštnih sporočil"
|
||||
},
|
||||
"description": "Prosimo, vnesite domeno, s katere Amazon pošilja e-pošto (npr.: amazon.com ali amazon.de)\n\nČe uporabljate preusmerjena e-poštna sporočila Amazon, ločite vsak naslov z vejico ali vnesite (noben) za izbris te nastavitve.\n\nOpomba: če ste v prejšnjem koraku konfigurirali glavo posredovanja, polje z naslovom posredovanja ne bo prikazano — glava samodejno obravnava ujemanje Amazonovih e-poštnih sporočil.",
|
||||
"title": "Nastavitve Amazon"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Mapa za shranjevanje slik"
|
||||
},
|
||||
"description": "Prosimo, vnesite imenik, v katerem želite shraniti svoje slike.\nPrivzeto je samodejno izpolnjeno.",
|
||||
"title": "Lokacija shranjevanja slik"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Mapa za shranjevanje slik"
|
||||
},
|
||||
"description": "Prosimo, vnesite imenik, v katerem želite shraniti svoje slike.\nPrivzeto je samodejno izpolnjeno.",
|
||||
"title": "Lokacija shranjevanja slik"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Glava za posredovanje (npr. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Posredovani e-poštni naslovi"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Če vaša storitev posredovanja vključuje izvirnega pošiljatelja v glavi, vnesite tukaj ime glave. To ima prednost pred spodnjim seznamom naslovov. Vnesite '(none)' ali pustite prazno, da namesto tega uporabite seznam naslovov.",
|
||||
"forwarded_emails": "Z vejicami ločen seznam naslovov za posredovanje, od katerih bodo prihajala sporočila. Pustite '(none)', če uporabljate zgornjo glavo za posredovanje."
|
||||
},
|
||||
"description": "Izberite, kako ujemati posredovana e-poštna sporočila.\n\n**Možnost 1 — Glava za posredovanje**: Če vaša storitev (npr. SimpleLogin) vključuje izvirnega pošiljatelja v glavo po meri, vnesite ime te glave. Mail and Packages jo bo uporabil za samodejno ujemanje naslovov prevoznikov.\n\n**Možnost 2 — Seznam naslovov**: Vnesite naslove za posredovanje, ki jih je vaša storitev dodelila vsakemu prevozniku, ločene z vejicami.",
|
||||
"title": "Nastavitve posredovane e-pošte"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Glava za posredovanje (npr. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Posredovani e-poštni naslovi"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Če vaša storitev posredovanja vključuje izvirnega pošiljatelja v glavi, vnesite tukaj ime glave. To ima prednost pred spodnjim seznamom naslovov. Vnesite '(none)' ali pustite prazno, da namesto tega uporabite seznam naslovov.",
|
||||
"forwarded_emails": "Z vejicami ločen seznam naslovov za posredovanje, od katerih bodo prihajala sporočila. Pustite '(none)', če uporabljate zgornjo glavo za posredovanje."
|
||||
},
|
||||
"description": "Izberite, kako ujemati posredovana e-poštna sporočila.\n\n**Možnost 1 — Glava za posredovanje**: Če vaša storitev (npr. SimpleLogin) vključuje izvirnega pošiljatelja v glavo po meri, vnesite ime te glave. Mail and Packages jo bo uporabil za samodejno ujemanje naslovov prevoznikov.\n\n**Možnost 2 — Seznam naslovov**: Vnesite naslove za posredovanje, ki jih je vaša storitev dodelila vsakemu prevozniku, ločene z vejicami.",
|
||||
"title": "Nastavitve posredovane e-pošte"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Prosimo, vnesite podatke o povezavi vašega poštnega strežnika.",
|
||||
"data": {
|
||||
"host": "Gostitelj",
|
||||
"password": "Geslo",
|
||||
"port": "Pristanišče",
|
||||
"username": "Uporabniško ime",
|
||||
"verify_ssl": "Preveri SSL certifikat",
|
||||
"imap_security": "Varnost IMAP"
|
||||
},
|
||||
"title": "Pošta in paketi (1. korak od 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Gostitelj",
|
||||
"password": "Geslo",
|
||||
"port": "Pristanišče",
|
||||
"username": "Uporabniško ime",
|
||||
"imap_security": "Varnost IMAP",
|
||||
"verify_ssl": "Preveri SSL certifikat"
|
||||
},
|
||||
"description": "Prosimo, vnesite podatke o povezavi vašega poštnega strežnika.",
|
||||
"title": "Pošta in paketi (1. korak od 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Endast en enda konfiguration av post och paket är tillåten.",
|
||||
"reconfigure_successful": "Omkonfigurering lyckades"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Det går inte att ansluta eller logga in på e-postservern. Kontrollera loggen för mer information.",
|
||||
"invalid_path": "Förvara bilderna i en annan katalog.",
|
||||
"ffmpeg_not_found": "Generera MP4 kräver ffmpeg",
|
||||
"amazon_domain": "Ogiltig vidarebefordrings e-postadress.",
|
||||
"file_not_found": "Bildfilen hittades inte",
|
||||
"invalid_email_format": "Ogiltigt format för e-postadress.",
|
||||
"missing_forwarded_emails": "Saknade vidarebefordrade e-postadresser. Du kan ange '(none)' för att rensa denna inställning.",
|
||||
"path_not_found": "Katalogen hittades inte",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "E-postmapp",
|
||||
"resources": "Sensorlista",
|
||||
"scan_interval": "Skanningsintervall (minuter)",
|
||||
"image_path": "Bildväg",
|
||||
"gif_duration": "Bildens längd (sekunder)",
|
||||
"image_security": "Slumpmässig bildfilnamn",
|
||||
"imap_timeout": "Tid i sekunder innan anslutningen kopplas från (sekunder, minst 10)",
|
||||
"generate_mp4": "Skapa mp4 från bilder",
|
||||
"allow_external": "Skapa bild för notifikationsappar",
|
||||
"custom_img": "Använd anpassad USPS 'inget post' bild?",
|
||||
"amazon_custom_img": "Använd anpassad Amazon 'ingen leverans' bild?",
|
||||
"ups_custom_img": "Använd anpassad UPS 'ingen leverans' bild?",
|
||||
"walmart_custom_img": "Använd anpassad Walmart 'ingen leverans' bild?",
|
||||
"fedex_custom_img": "Använd anpassad FedEx 'ingen leverans' bild?",
|
||||
"generic_custom_img": "Använd anpassad generisk 'ingen leverans' bild?",
|
||||
"generate_grid": "Skapa bildrutnät för LLM-visionsmodeller",
|
||||
"allow_forwarded_emails": "Tillåt vidarebefordrade e-postmeddelanden utöver en tjänsts standardvärde (t.ex. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Avsluta konfigurationen genom att anpassa följande baserat på din e-poststruktur och installation av hemassistent. \n\n Mer information om alternativen [Mail and Packages] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) läser [konfiguration, mallar , och automatiseringsavsnitt] (https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) på GitHub.",
|
||||
"title": "Mail och paket (steg 2 av 2)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Sökväg till anpassad bild: (t.ex: bilder/min_anpassade_ingen_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Ange sökvägen och filnamnet till din anpassade ingen post-bild nedan.\n\nExempel: bilder/anpassad_nomail.gif",
|
||||
"title": "Post och paket (Steg 3 av 3)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domän",
|
||||
"amazon_fwds": "Amazon vidarebefordrade e-postadresser",
|
||||
"amazon_days": "Dagar tillbaka för att kontrollera Amazon-e-postmeddelanden"
|
||||
},
|
||||
"description": "Vänligen ange domänen som Amazon skickar e-postmeddelanden från (t.ex: amazon.com eller amazon.de)\n\nOm du använder Amazon vidarebefordrade e-postmeddelanden, vänligen separera varje adress med ett kommatecken eller ange (ingen) för att rensa denna inställning.\n\nObs: om du konfigurerade ett vidarebefordringshuvud i föregående steg kommer fältet för vidarebefordringsadress inte att visas — huvudet hanterar Amazon e-postkopplingen automatiskt.",
|
||||
"title": "Amazon-inställningar"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "E-postmapp",
|
||||
"scan_interval": "Skanningsintervall (minuter)",
|
||||
"image_path": "Bildväg",
|
||||
"gif_duration": "Bildens längd (sekunder)",
|
||||
"image_security": "Slumpmässig bildfilnamn",
|
||||
"generate_mp4": "Skapa mp4 från bilder",
|
||||
"resources": "Sensorlista",
|
||||
"imap_timeout": "Tid i sekunder innan anslutningen kopplas från (sekunder, minst 10)",
|
||||
"allow_external": "Skapa bild för notifikationsappar",
|
||||
"custom_img": "Använd anpassad USPS 'inget post' bild?",
|
||||
"amazon_custom_img": "Använd anpassad Amazon 'ingen leverans' bild?",
|
||||
"ups_custom_img": "Använd anpassad UPS 'ingen leverans' bild?",
|
||||
"walmart_custom_img": "Använd anpassad Walmart 'ingen leverans' bild?",
|
||||
"fedex_custom_img": "Använd anpassad FedEx 'ingen leverans' bild?",
|
||||
"generic_custom_img": "Använd anpassad generisk 'ingen leverans' bild?",
|
||||
"generate_grid": "Skapa bildrutnät för LLM-visionsmodeller",
|
||||
"allow_forwarded_emails": "Tillåt vidarebefordrade e-postmeddelanden utöver en tjänsts standardvärde (t.ex. no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "Slutför konfigurationen genom att anpassa följande baserat på din e-poststruktur och Home Assistant-installation.\n\nFör detaljer om [Mail and Packages integration](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) alternativen granska [konfiguration, mallar och automatiseringssektionen](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration) på GitHub.\n\nOm du använder vidarebefordrade e-postmeddelanden från Amazon, separera varje adress med ett kommatecken eller ange (ingen) för att rensa denna inställning.",
|
||||
"title": "Mail och paket (steg 2 av 2)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Sökväg till anpassad bild: (t.ex.: bilder/min_anpassade_ingen_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "Ange sökvägen och filnamnet till din anpassade ingen post-bild nedan.\n\nExempel: bilder/anpassad_nomail.gif",
|
||||
"title": "Post och paket (Steg 3 av 3)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon domän",
|
||||
"amazon_fwds": "Amazon vidarebefordrade e-postadresser",
|
||||
"amazon_days": "Dagar tillbaka för att kontrollera Amazon-e-postmeddelanden"
|
||||
},
|
||||
"description": "Vänligen ange domänen som Amazon skickar e-postmeddelanden från (t.ex: amazon.com eller amazon.de)\n\nOm du använder Amazon vidarebefordrade e-postmeddelanden, vänligen separera varje adress med ett kommatecken eller ange (ingen) för att rensa denna inställning.\n\nObs: om du konfigurerade ett vidarebefordringshuvud i föregående steg kommer fältet för vidarebefordringsadress inte att visas — huvudet hanterar Amazon e-postkopplingen automatiskt.",
|
||||
"title": "Amazon-inställningar"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "Katalog för att lagra bilder"
|
||||
},
|
||||
"description": "Vänligen ange den katalog du vill att dina bilder ska lagras i.\nStandardinställningen är automatiskt ifylld.",
|
||||
"title": "Bildlagringsplats"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "Katalog för att lagra bilder"
|
||||
},
|
||||
"description": "Vänligen ange den katalog du vill att dina bilder ska lagras i.\nStandardinställningen är automatiskt ifylld.",
|
||||
"title": "Bildlagringsplats"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Vidarebefordringshuvud (t.ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Vidarebefordrade e-postadresser"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Om din vidarebefordringstjänst inkluderar den ursprungliga avsändaren i ett huvud, ange huvud-namnet här. Detta har företräde framför adresslistan nedan. Ange '(none)' eller lämna tomt för att använda adresslistan istället.",
|
||||
"forwarded_emails": "Kommaseparerad lista över vidarebefordringsadresser som meddelanden kommer att anlända från. Lämna '(none)' om du använder vidarebefordringshuvudet ovan."
|
||||
},
|
||||
"description": "Välj hur vidarebefordrade e-postmeddelanden ska matchas.\n\n**Alternativ 1 — Vidarebefordringshuvud**: Om din tjänst (t.ex. SimpleLogin) inkluderar den ursprungliga avsändaren i ett anpassat huvud, ange det huvud-namnet. Mail and Packages kommer att använda det för att automatiskt matcha transportörernas adresser.\n\n**Alternativ 2 — Adresslista**: Ange de vidarebefordringsadresser som din tjänst har tilldelat varje transportör, separerade med kommatecken.",
|
||||
"title": "Inställningar för vidarebefordrade e-postmeddelanden"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "Vidarebefordringshuvud (t.ex. X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "Vidarebefordrade e-postadresser"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "Om din vidarebefordringstjänst inkluderar den ursprungliga avsändaren i ett huvud, ange huvud-namnet här. Detta har företräde framför adresslistan nedan. Ange '(none)' eller lämna tomt för att använda adresslistan istället.",
|
||||
"forwarded_emails": "Kommaseparerad lista över vidarebefordringsadresser som meddelanden kommer att anlända från. Lämna '(none)' om du använder vidarebefordringshuvudet ovan."
|
||||
},
|
||||
"description": "Välj hur vidarebefordrade e-postmeddelanden ska matchas.\n\n**Alternativ 1 — Vidarebefordringshuvud**: Om din tjänst (t.ex. SimpleLogin) inkluderar den ursprungliga avsändaren i ett anpassat huvud, ange det huvud-namnet. Mail and Packages kommer att använda det för att automatiskt matcha transportörernas adresser.\n\n**Alternativ 2 — Adresslista**: Ange de vidarebefordringsadresser som din tjänst har tilldelat varje transportör, separerade med kommatecken.",
|
||||
"title": "Inställningar för vidarebefordrade e-postmeddelanden"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "Vänligen ange anslutningsinformationen för din e-postserver.",
|
||||
"data": {
|
||||
"host": "Värd",
|
||||
"password": "Lösenord",
|
||||
"port": "Hamn",
|
||||
"username": "Användarnamn",
|
||||
"verify_ssl": "Verifiera SSL-certifikat",
|
||||
"imap_security": "IMAP-säkerhet"
|
||||
},
|
||||
"title": "Mail och paket (steg 1 av 2)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "Värd",
|
||||
"password": "Lösenord",
|
||||
"port": "Hamn",
|
||||
"username": "Användarnamn",
|
||||
"imap_security": "IMAP-säkerhet",
|
||||
"verify_ssl": "Verifiera SSL-certifikat"
|
||||
},
|
||||
"description": "Vänligen ange anslutningsinformationen för din e-postserver.",
|
||||
"title": "Mail och paket (steg 1 av 2)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "僅允許郵件和軟件包的單個配置。",
|
||||
"reconfigure_successful": "重新配置成功"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "無法連接或登錄到郵件服務器。請檢查日誌以獲取詳細信息。",
|
||||
"invalid_path": "請將圖像存儲在另一個目錄中。",
|
||||
"ffmpeg_not_found": "生成MP4需要ffmpeg",
|
||||
"amazon_domain": "無效的轉發電郵地址。",
|
||||
"file_not_found": "找不到圖像檔案",
|
||||
"invalid_email_format": "無效的電郵地址格式。",
|
||||
"missing_forwarded_emails": "缺少轉發的電郵地址。您可以輸入 '(none)' 來清除此設定。",
|
||||
"path_not_found": "找不到目錄",
|
||||
"invalid_auth": "Invalid authentication details. Please check your username and password.",
|
||||
"ssl_error": "An SSL error occurred. Change SSL cipher list and try again."
|
||||
},
|
||||
"step": {
|
||||
"config_2": {
|
||||
"data": {
|
||||
"folder": "郵件文件夾",
|
||||
"resources": "感應器列表",
|
||||
"scan_interval": "掃描間隔(分鐘)",
|
||||
"image_path": "圖像路徑",
|
||||
"gif_duration": "圖像持續時間(秒)",
|
||||
"image_security": "隨機圖像文件名",
|
||||
"imap_timeout": "連接超時前的時間(秒,最少10秒)",
|
||||
"generate_mp4": "從圖像創建mp4",
|
||||
"allow_external": "為通知應用程式創建圖像",
|
||||
"custom_img": "使用自訂的 USPS「無郵件」圖像?",
|
||||
"amazon_custom_img": "使用自訂的 Amazon「無送貨」圖像?",
|
||||
"ups_custom_img": "使用自訂的 UPS「無送貨」圖像?",
|
||||
"walmart_custom_img": "使用自訂的 Walmart「無送貨」圖像?",
|
||||
"fedex_custom_img": "使用自訂的 FedEx「無送貨」圖像?",
|
||||
"generic_custom_img": "使用自訂的通用「無送貨」圖像?",
|
||||
"generate_grid": "為LLM視覺模型創建圖像網格",
|
||||
"allow_forwarded_emails": "允許轉發的電郵,除了服務的預設值(例如 no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "通過根據您的電子郵件結構和Home Assistant安裝自定義以下內容來完成配置。 \n\n有關[郵件和軟件包集成](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration)選項的詳細信息,請查看[配置,模板和自動化部分](GitHub上的https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration)。",
|
||||
"title": "郵件和包裹(第2步,共2步)"
|
||||
},
|
||||
"config_3": {
|
||||
"data": {
|
||||
"custom_img_file": "自訂圖片的路徑:(例如:images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (ie: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (ie: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (ie: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (ie: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "在下方輸入您的自訂無郵件圖像的路徑和檔案名稱。\n\n例如:images/custom_nomail.gif",
|
||||
"title": "郵件和包裹(第3步,共3步)"
|
||||
},
|
||||
"config_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon 域名",
|
||||
"amazon_fwds": "Amazon 轉發的電郵地址",
|
||||
"amazon_days": "檢查Amazon電郵的天數"
|
||||
},
|
||||
"description": "請輸入亞馬遜發送電子郵件的域名(例如:amazon.com或amazon.de)\n\n如果使用亞馬遜轉發的電子郵件,請用逗號分隔每個地址,或輸入(無)以清除此設定。\n\n注意:如果您在上一步中設定了轉寄標頭,轉寄地址欄位將不會顯示 — 標頭會自動處理 Amazon 電郵的配對。",
|
||||
"title": "Amazon 設定"
|
||||
},
|
||||
"reconfig_2": {
|
||||
"data": {
|
||||
"folder": "郵件文件夾",
|
||||
"scan_interval": "掃描間隔(分鐘)",
|
||||
"image_path": "圖像路徑",
|
||||
"gif_duration": "圖像持續時間(秒)",
|
||||
"image_security": "隨機圖像文件名",
|
||||
"generate_mp4": "從圖像創建mp4",
|
||||
"resources": "感應器列表",
|
||||
"imap_timeout": "連接超時前的時間(秒,最少10秒)",
|
||||
"allow_external": "為通知應用程式創建圖像",
|
||||
"custom_img": "使用自訂的 USPS「無郵件」圖像?",
|
||||
"amazon_custom_img": "使用自訂的 Amazon「無送貨」圖像?",
|
||||
"ups_custom_img": "使用自訂的 UPS「無送貨」圖像?",
|
||||
"walmart_custom_img": "使用自訂的 Walmart「無送貨」圖像?",
|
||||
"fedex_custom_img": "使用自訂的 FedEx「無送貨」圖像?",
|
||||
"generic_custom_img": "使用自訂的通用「無送貨」圖像?",
|
||||
"generate_grid": "為LLM視覺模型創建圖像網格",
|
||||
"allow_forwarded_emails": "允許轉發的電郵,除了服務的預設值(例如 no-reply@usps.com)",
|
||||
"custom_days": "Days back to check for package emails (minimum 1, default 3)"
|
||||
},
|
||||
"description": "根據您的電郵結構和Home Assistant安裝來完成配置。\n\n有關[郵件和包裹整合](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration)選項的詳情,請查閱GitHub上的[配置、模板和自動化部分](https://github.com/moralmunky/Home-Assistant-Mail-And-Packages/wiki/Configuration-and-Email-Settings#configuration)。\n\n如果使用Amazon轉發的電郵,請用逗號分隔每個地址,或輸入(none)以清除此設定。",
|
||||
"title": "郵件和包裹(第2步,共2步)"
|
||||
},
|
||||
"reconfig_3": {
|
||||
"data": {
|
||||
"custom_img_file": "Path to custom image: (i.e.: images/my_custom_no_mail.jpg)",
|
||||
"amazon_custom_img_file": "Path to custom Amazon image: (i.e.: images/my_custom_no_amazon.jpg)",
|
||||
"ups_custom_img_file": "Path to custom UPS image: (i.e.: images/my_custom_no_ups.jpg)",
|
||||
"walmart_custom_img_file": "Path to custom Walmart image: (i.e.: images/my_custom_no_walmart.jpg)",
|
||||
"fedex_custom_img_file": "Path to custom FedEx image: (i.e.: images/my_custom_no_fedex.jpg)"
|
||||
},
|
||||
"description": "在下方輸入您的自訂無郵件圖像的路徑和檔案名稱。\n\n例如:images/custom_nomail.gif",
|
||||
"title": "郵件和包裹(第3步,共3步)"
|
||||
},
|
||||
"reconfig_amazon": {
|
||||
"data": {
|
||||
"amazon_domain": "Amazon 域名",
|
||||
"amazon_fwds": "Amazon 轉發的電郵地址",
|
||||
"amazon_days": "檢查Amazon電郵的天數"
|
||||
},
|
||||
"description": "請輸入亞馬遜發送電子郵件的域名(例如:amazon.com或amazon.de)\n\n如果使用亞馬遜轉發的電子郵件,請用逗號分隔每個地址,或輸入(無)以清除此設定。\n\n注意:如果您在上一步中設定了轉寄標頭,轉寄地址欄位將不會顯示 — 標頭會自動處理 Amazon 電郵的配對。",
|
||||
"title": "Amazon 設定"
|
||||
},
|
||||
"config_storage": {
|
||||
"data": {
|
||||
"storage": "儲存圖片的目錄"
|
||||
},
|
||||
"description": "請輸入您希望存儲圖像的目錄。\n預設值會自動填充。",
|
||||
"title": "圖像儲存位置"
|
||||
},
|
||||
"reconfig_storage": {
|
||||
"data": {
|
||||
"storage": "儲存圖片的目錄"
|
||||
},
|
||||
"description": "請輸入您希望存儲圖像的目錄。\n預設值會自動填充。",
|
||||
"title": "圖像儲存位置"
|
||||
},
|
||||
"config_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "轉寄標頭 (例如 X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "轉發的電郵地址"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "如果您的轉寄服務在標頭中包含原始寄件者,請在此輸入標頭名稱。這優先於下方的地址清單。輸入「(none)」或留空以改用地址清單。",
|
||||
"forwarded_emails": "以逗號分隔的轉寄地址清單,訊息將從這些地址發送。如果使用上方的轉寄標頭,請保留「(none)」。"
|
||||
},
|
||||
"description": "選擇如何匹配轉寄的電子郵件。\n\n**選項 1 — 轉寄標頭**:如果您的服務(例如 SimpleLogin)在自訂標頭中包含原始寄件者,請輸入該標頭名稱。Mail and Packages 將使用它自動匹配快遞公司地址。\n\n**選項 2 — 地址清單**:輸入您的服務為每個快遞公司分配的轉寄地址,以逗號分隔。",
|
||||
"title": "轉寄電子郵件設定"
|
||||
},
|
||||
"reconfig_forwarded_emails": {
|
||||
"data": {
|
||||
"forwarding_header": "轉寄標頭 (例如 X-SimpleLogin-Original-From)",
|
||||
"forwarded_emails": "轉發的電郵地址"
|
||||
},
|
||||
"data_description": {
|
||||
"forwarding_header": "如果您的轉寄服務在標頭中包含原始寄件者,請在此輸入標頭名稱。這優先於下方的地址清單。輸入「(none)」或留空以改用地址清單。",
|
||||
"forwarded_emails": "以逗號分隔的轉寄地址清單,訊息將從這些地址發送。如果使用上方的轉寄標頭,請保留「(none)」。"
|
||||
},
|
||||
"description": "選擇如何匹配轉寄的電子郵件。\n\n**選項 1 — 轉寄標頭**:如果您的服務(例如 SimpleLogin)在自訂標頭中包含原始寄件者,請輸入該標頭名稱。Mail and Packages 將使用它自動匹配快遞公司地址。\n\n**選項 2 — 地址清單**:輸入您的服務為每個快遞公司分配的轉寄地址,以逗號分隔。",
|
||||
"title": "轉寄電子郵件設定"
|
||||
},
|
||||
"imap_config": {
|
||||
"description": "請輸入您的郵件伺服器的連接資訊。",
|
||||
"data": {
|
||||
"host": "主辦",
|
||||
"password": "密碼",
|
||||
"port": "港口",
|
||||
"username": "用戶名",
|
||||
"verify_ssl": "驗證 SSL 證書",
|
||||
"imap_security": "IMAP 安全性"
|
||||
},
|
||||
"title": "郵件和包裹(第1步,共2步)"
|
||||
},
|
||||
"user": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reconfig_imap": {
|
||||
"data": {
|
||||
"host": "主辦",
|
||||
"password": "密碼",
|
||||
"port": "港口",
|
||||
"username": "用戶名",
|
||||
"imap_security": "IMAP 安全性",
|
||||
"verify_ssl": "驗證 SSL 證書"
|
||||
},
|
||||
"description": "請輸入您的郵件伺服器的連接資訊。",
|
||||
"title": "郵件和包裹(第1步,共2步)"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Select the authentication method for your mail server.",
|
||||
"data": {
|
||||
"auth_type": "Authentication Type"
|
||||
},
|
||||
"title": "Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The authentication for your mail server has expired or is invalid. Please re-authenticate to continue.",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Re-authenticate Mail Server"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"auth_type": {
|
||||
"options": {
|
||||
"password": "Password",
|
||||
"oauth2_microsoft": "OAuth2 - Microsoft (Outlook/Exchange)",
|
||||
"oauth2_google": "OAuth2 - Google (Gmail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""Utility functions for Mail and Packages."""
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Amazon specific utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import email
|
||||
import logging
|
||||
import re
|
||||
from email.header import decode_header
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import dateparser
|
||||
from aioimaplib import IMAP4_SSL
|
||||
|
||||
from custom_components.mail_and_packages.const import (
|
||||
AMAZON_DELIVERED_SUBJECT,
|
||||
AMAZON_DOMAINS,
|
||||
AMAZON_EMAIL,
|
||||
AMAZON_IMG_LIST,
|
||||
AMAZON_IMG_PATTERN,
|
||||
AMAZON_ORDERED_SUBJECT,
|
||||
AMAZON_SHIPMENT_SUBJECT,
|
||||
AMAZON_SHIPMENT_TRACKING,
|
||||
AMAZON_TIME_PATTERN,
|
||||
AMAZON_TIME_PATTERN_END,
|
||||
AMAZON_TIME_PATTERN_REGEX,
|
||||
DEFAULT_AMAZON_DAYS,
|
||||
)
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
from custom_components.mail_and_packages.utils.date import get_today
|
||||
from custom_components.mail_and_packages.utils.image import io_save_file
|
||||
from custom_components.mail_and_packages.utils.imap import (
|
||||
email_fetch,
|
||||
email_search,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
DOMAIN_LANG_MAP = {
|
||||
"amazon.de": ["versandbestaetigung", "Geliefert:", "Zugestellt:"],
|
||||
"amazon.it": ["conferma-spedizione", "Consegna effettuata:", "Arriverà"],
|
||||
"amazon.nl": [
|
||||
"update-bestelling",
|
||||
"verzending-volgen",
|
||||
"auto-bevestiging",
|
||||
"Bezorgd:",
|
||||
],
|
||||
"amazon.fr": ["confirmation-commande", "Livré", "Livraison : Votre", "Arrivée :"],
|
||||
"amazon.ca": ["confirmation-commande", "Livré", "Livraison : Votre", "Arrivée :"],
|
||||
"amazon.es": [
|
||||
"confirmar-envio",
|
||||
"Entregado:",
|
||||
"Enviado:",
|
||||
"Pedido efetuado:",
|
||||
"Chega ",
|
||||
],
|
||||
"amazon.pl": ["Dostarczono:"],
|
||||
}
|
||||
|
||||
|
||||
def filter_amazon_strings(strings: list[str], domain: str) -> list[str]:
|
||||
"""Filter list of strings based on the domain language."""
|
||||
all_mapped_strings = []
|
||||
for lang_list in DOMAIN_LANG_MAP.values():
|
||||
all_mapped_strings.extend(lang_list)
|
||||
|
||||
def is_mapped(s: str) -> bool:
|
||||
return any(m in s for m in all_mapped_strings)
|
||||
|
||||
base_strings = [s for s in strings if not is_mapped(s)]
|
||||
|
||||
domain_strings = []
|
||||
if domain in DOMAIN_LANG_MAP:
|
||||
mapped_for_domain = DOMAIN_LANG_MAP[domain]
|
||||
domain_strings = [s for s in strings if any(m in s for m in mapped_for_domain)]
|
||||
|
||||
# Only amazon.ca and unmapped domains (like .com, .co.uk) should use base English strings
|
||||
if domain in DOMAIN_LANG_MAP and domain != "amazon.ca":
|
||||
return list(dict.fromkeys(domain_strings))
|
||||
|
||||
return list(dict.fromkeys(base_strings + domain_strings))
|
||||
|
||||
|
||||
def get_decoded_subject(msg: email.message.Message) -> str:
|
||||
"""Decode email subject."""
|
||||
header_val = msg["subject"]
|
||||
if header_val:
|
||||
decoded_parts = []
|
||||
for subject_bytes, encoding in decode_header(header_val):
|
||||
if isinstance(subject_bytes, bytes):
|
||||
if encoding:
|
||||
try:
|
||||
decoded_parts.append(subject_bytes.decode(encoding, "ignore"))
|
||||
continue
|
||||
except (LookupError, UnicodeError):
|
||||
pass
|
||||
decoded_parts.append(subject_bytes.decode("utf-8", "ignore"))
|
||||
else:
|
||||
decoded_parts.append(str(subject_bytes))
|
||||
return "".join(decoded_parts)
|
||||
|
||||
body = get_email_body(msg)
|
||||
if not body:
|
||||
return ""
|
||||
title_match = re.search(
|
||||
r"<title[^>]*>([^<]+)</title>", body, re.IGNORECASE | re.DOTALL
|
||||
)
|
||||
if not title_match:
|
||||
return ""
|
||||
return title_match.group(1).strip()
|
||||
|
||||
|
||||
def get_email_body(msg: email.message.Message) -> str:
|
||||
"""Extract and decode the email body safely."""
|
||||
try:
|
||||
if msg.is_multipart():
|
||||
# Standard practice is to look for text/plain then text/html
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
return part.get_payload(decode=True).decode("utf-8", "ignore")
|
||||
# If no text/plain, fall back to first part
|
||||
payload = msg.get_payload(0)
|
||||
if isinstance(payload, email.message.Message):
|
||||
return payload.get_payload(decode=True).decode("utf-8", "ignore")
|
||||
return str(payload)
|
||||
|
||||
return msg.get_payload(decode=True).decode("utf-8", "ignore")
|
||||
except (ValueError, TypeError, IndexError, AttributeError) as err:
|
||||
_LOGGER.debug("Problem decoding email message: %s", err)
|
||||
return ""
|
||||
|
||||
|
||||
def extract_order_numbers(text: str, pattern: re.Pattern | str) -> list[str]:
|
||||
"""Extract order numbers from text."""
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
return pattern.findall(text)
|
||||
|
||||
|
||||
async def parse_amazon_arrival_date(
|
||||
hass: Any,
|
||||
email_msg: str,
|
||||
email_date: datetime.date,
|
||||
) -> datetime.date | None:
|
||||
"""Determine arrival date from email."""
|
||||
today_date = get_today()
|
||||
|
||||
# Try using regex for more precise extraction of the arrival date string
|
||||
if date_str := amazon_date_regex(email_msg):
|
||||
base_datetime = datetime.datetime.combine(
|
||||
email_date or today_date,
|
||||
datetime.time(),
|
||||
)
|
||||
|
||||
# 1. Try parsing without PREFER_DATES_FROM: future to handle relative terms (any language)
|
||||
dateobj = await hass.async_add_executor_job(
|
||||
partial(
|
||||
dateparser.parse,
|
||||
date_str,
|
||||
settings={
|
||||
"RELATIVE_BASE": base_datetime,
|
||||
"RETURN_AS_TIMEZONE_AWARE": False,
|
||||
},
|
||||
),
|
||||
)
|
||||
if dateobj:
|
||||
parsed_date = dateobj.date()
|
||||
base_date = email_date or today_date
|
||||
# Only accept matches that resolve to email_date (today) or email_date + 1 day (tomorrow)
|
||||
if (
|
||||
parsed_date == base_date
|
||||
or parsed_date == base_date + datetime.timedelta(days=1)
|
||||
):
|
||||
return parsed_date
|
||||
|
||||
# 2. Fall back to parsing with PREFER_DATES_FROM: future for absolute dates
|
||||
dateobj = await hass.async_add_executor_job(
|
||||
partial(
|
||||
dateparser.parse,
|
||||
date_str,
|
||||
settings={
|
||||
"PREFER_DATES_FROM": "future",
|
||||
"RELATIVE_BASE": base_datetime,
|
||||
"RETURN_AS_TIMEZONE_AWARE": False,
|
||||
},
|
||||
),
|
||||
)
|
||||
if dateobj:
|
||||
return dateobj.date()
|
||||
|
||||
# Fallback to chunk-based parsing
|
||||
for search in AMAZON_TIME_PATTERN:
|
||||
if search not in email_msg:
|
||||
continue
|
||||
|
||||
start = email_msg.find(search) + len(search)
|
||||
chunk = email_msg[start : start + 50]
|
||||
|
||||
dateobj = await hass.async_add_executor_job(
|
||||
partial(
|
||||
dateparser.parse,
|
||||
chunk,
|
||||
settings={
|
||||
"PREFER_DATES_FROM": "future",
|
||||
"RELATIVE_BASE": datetime.datetime.combine(
|
||||
email_date or today_date,
|
||||
datetime.time(),
|
||||
),
|
||||
"RETURN_AS_TIMEZONE_AWARE": False,
|
||||
},
|
||||
),
|
||||
)
|
||||
if dateobj:
|
||||
return dateobj.date()
|
||||
return None
|
||||
|
||||
|
||||
def amazon_email_addresses(
|
||||
fwds: list[str] | str | None = None,
|
||||
domain: str | None = None,
|
||||
) -> list[str]:
|
||||
"""Generate Amazon email addresses."""
|
||||
if isinstance(fwds, str):
|
||||
fwds = [fwds]
|
||||
elif not isinstance(fwds, (list, tuple)):
|
||||
fwds = None
|
||||
|
||||
if domain is None:
|
||||
domain = "amazon.com"
|
||||
|
||||
# Use both AMAZON_EMAIL and AMAZON_SHIPMENT_TRACKING for prefixes
|
||||
prefixes = list(AMAZON_EMAIL)
|
||||
for p in AMAZON_SHIPMENT_TRACKING:
|
||||
if f"{p}@" not in prefixes:
|
||||
prefixes.append(f"{p}@")
|
||||
|
||||
prefixes = filter_amazon_strings(prefixes, domain)
|
||||
|
||||
value = [f"{e}{domain}" for e in prefixes]
|
||||
if fwds:
|
||||
for fwd in fwds:
|
||||
if "@" in fwd:
|
||||
value.append(fwd)
|
||||
elif any(f in fwd for f in AMAZON_DOMAINS):
|
||||
value.extend(f"{e}{fwd}" for e in prefixes)
|
||||
return value
|
||||
|
||||
|
||||
async def search_amazon_emails(
|
||||
account: IMAP4_SSL,
|
||||
address_list: list[str],
|
||||
days: int,
|
||||
domain: str | None = None,
|
||||
cache: EmailCache | None = None,
|
||||
forwarding_header: str = "",
|
||||
) -> list[bytes]:
|
||||
"""Search for Amazon emails."""
|
||||
if not isinstance(days, int):
|
||||
try:
|
||||
days = int(days)
|
||||
except (ValueError, TypeError):
|
||||
days = DEFAULT_AMAZON_DAYS
|
||||
|
||||
past_date = get_today() - datetime.timedelta(days=days)
|
||||
tfmt = past_date.strftime("%d-%b-%Y")
|
||||
amazon_subjects = (
|
||||
AMAZON_DELIVERED_SUBJECT + AMAZON_SHIPMENT_SUBJECT + AMAZON_ORDERED_SUBJECT
|
||||
)
|
||||
if domain:
|
||||
amazon_subjects = filter_amazon_strings(amazon_subjects, domain)
|
||||
|
||||
(server_response, sdata) = await email_search(
|
||||
account=account,
|
||||
address=address_list,
|
||||
date=tfmt,
|
||||
subject=amazon_subjects,
|
||||
header=forwarding_header,
|
||||
)
|
||||
|
||||
if server_response != "OK" or not sdata[0]:
|
||||
return []
|
||||
|
||||
return sdata[0].split()
|
||||
|
||||
|
||||
async def download_amazon_img(
|
||||
img_url: str,
|
||||
img_path: str,
|
||||
img_name: str,
|
||||
hass: Any,
|
||||
) -> None:
|
||||
"""Download image from url."""
|
||||
img_path = Path(img_path) / "amazon"
|
||||
filepath = img_path / img_name
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
try:
|
||||
async with session.get(img_url.replace("&", "&")) as resp:
|
||||
if resp.status != 200:
|
||||
return
|
||||
content_type = resp.headers.get("content-type", "")
|
||||
if "image" not in content_type:
|
||||
return
|
||||
content_length = int(resp.headers.get("content-length", 0))
|
||||
if content_length > _MAX_IMAGE_SIZE:
|
||||
_LOGGER.warning(
|
||||
"Amazon image too large to download (%d bytes), skipping",
|
||||
content_length,
|
||||
)
|
||||
return
|
||||
data = await resp.read()
|
||||
if len(data) > _MAX_IMAGE_SIZE:
|
||||
_LOGGER.warning(
|
||||
"Amazon image exceeds size limit after download, discarding"
|
||||
)
|
||||
return
|
||||
await hass.async_add_executor_job(io_save_file, filepath, data)
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Problem downloading file: %s", err)
|
||||
|
||||
|
||||
async def get_amazon_image_urls(
|
||||
sdata: Any,
|
||||
account: IMAP4_SSL,
|
||||
cache: EmailCache | None = None,
|
||||
) -> list[str]:
|
||||
"""Find all Amazon delivery image URLs."""
|
||||
mail_list = sdata.split()
|
||||
pattern = re.compile(rf"{AMAZON_IMG_PATTERN}")
|
||||
urls = []
|
||||
for i in mail_list:
|
||||
if cache:
|
||||
data = (await cache.fetch(i, "(RFC822)"))[1]
|
||||
else:
|
||||
data = (await email_fetch(account, i, "(RFC822)"))[1]
|
||||
for response_part in data:
|
||||
if isinstance(response_part, (bytes, bytearray)):
|
||||
msg = email.message_from_bytes(response_part)
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() != "text/html":
|
||||
continue
|
||||
part_payload = part.get_payload(decode=True)
|
||||
part_content = part_payload.decode("utf-8", "ignore")
|
||||
found = pattern.findall(part_content)
|
||||
for url in found:
|
||||
if url[1] not in AMAZON_IMG_LIST:
|
||||
continue
|
||||
full_url = url[0] + url[1] + url[2]
|
||||
if full_url not in urls:
|
||||
urls.append(full_url)
|
||||
return urls
|
||||
|
||||
|
||||
def _extract_hub_code(
|
||||
body: str,
|
||||
hub_pattern: str,
|
||||
subject: str,
|
||||
subject_pattern: str,
|
||||
) -> str:
|
||||
"""Extract Amazon Hub code from email body or subject."""
|
||||
# Check subject first
|
||||
if (found := re.compile(subject_pattern).search(subject)) is not None:
|
||||
return found.group(3)
|
||||
|
||||
# Check body
|
||||
if (found := re.compile(hub_pattern).search(body)) is not None:
|
||||
return found.group(2)
|
||||
return ""
|
||||
|
||||
|
||||
def amazon_date_search(email_msg: str, patterns: list[str] | None = None) -> int:
|
||||
"""Search for a date pattern in an email message and return its index."""
|
||||
if patterns is None:
|
||||
patterns = AMAZON_TIME_PATTERN_END
|
||||
|
||||
for pattern in patterns:
|
||||
if (index := email_msg.find(pattern)) != -1:
|
||||
return index
|
||||
return -1
|
||||
|
||||
|
||||
def amazon_date_regex(email_msg: str, patterns: list[str] | None = None) -> str | None:
|
||||
"""Search for a date pattern using regex and return the first capture group."""
|
||||
if patterns is None:
|
||||
patterns = AMAZON_TIME_PATTERN_REGEX
|
||||
|
||||
for pattern in patterns:
|
||||
if (found := re.compile(pattern, re.IGNORECASE).search(email_msg)) is not None:
|
||||
if found.groups():
|
||||
return found.group(1)
|
||||
return None
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Email caching utility for Mail and Packages."""
|
||||
|
||||
import logging
|
||||
|
||||
from aioimaplib import IMAP4_SSL
|
||||
|
||||
from .imap import email_fetch, email_fetch_batch, email_fetch_headers, email_fetch_text
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailCache:
|
||||
"""Cache for IMAP email fetches to avoid duplicate downloads."""
|
||||
|
||||
def __init__(self, account: IMAP4_SSL) -> None:
|
||||
"""Initialize the cache."""
|
||||
self.account = account
|
||||
self._cache_rfc822: dict[str, tuple] = {}
|
||||
self._cache_text: dict[str, tuple] = {}
|
||||
self._cache_headers: dict[str, tuple] = {}
|
||||
|
||||
async def fetch(self, email_id: str | bytes, parts: str = "(RFC822)") -> tuple: # noqa: C901
|
||||
"""Fetch email content or return from cache."""
|
||||
eid_str = email_id.decode() if isinstance(email_id, bytes) else str(email_id)
|
||||
|
||||
if parts in ("(RFC822)", "BODY[]"):
|
||||
if eid_str in self._cache_rfc822:
|
||||
return self._cache_rfc822[eid_str]
|
||||
res = await email_fetch(self.account, eid_str, parts)
|
||||
if res[0] == "OK":
|
||||
self._cache_rfc822[eid_str] = res
|
||||
return res
|
||||
|
||||
if "HEADER" in parts:
|
||||
if eid_str in self._cache_headers:
|
||||
return self._cache_headers[eid_str]
|
||||
# fallback to RFC822 cache if we already have it
|
||||
if eid_str in self._cache_rfc822:
|
||||
return self._cache_rfc822[eid_str]
|
||||
res = await email_fetch_headers(self.account, eid_str)
|
||||
if res[0] == "OK":
|
||||
self._cache_headers[eid_str] = res
|
||||
return res
|
||||
|
||||
if "TEXT" in parts or parts == "(BODY[1])":
|
||||
if eid_str in self._cache_text:
|
||||
return self._cache_text[eid_str]
|
||||
if eid_str in self._cache_rfc822:
|
||||
return self._cache_rfc822[eid_str]
|
||||
res = await email_fetch_text(self.account, eid_str, parts)
|
||||
if res[0] == "OK":
|
||||
self._cache_text[eid_str] = res
|
||||
return res
|
||||
|
||||
# Unknown part, bypass cache
|
||||
return await email_fetch(self.account, eid_str, parts)
|
||||
|
||||
async def fetch_batch(
|
||||
self, email_ids: list[str | bytes], parts: str = "(RFC822)"
|
||||
) -> tuple:
|
||||
"""Fetch multiple emails in a batch, retrieving from cache if applicable."""
|
||||
# For simplicity, if we don't have all of them, fetch the missing ones in batch
|
||||
missing_ids = []
|
||||
eid_strs = [e.decode() if isinstance(e, bytes) else str(e) for e in email_ids]
|
||||
|
||||
# Determine which ones we already have
|
||||
for eid_str in eid_strs:
|
||||
if parts in ("(RFC822)", "BODY[]"):
|
||||
if eid_str not in self._cache_rfc822:
|
||||
missing_ids.append(eid_str)
|
||||
elif "HEADER" in parts:
|
||||
if (
|
||||
eid_str not in self._cache_headers
|
||||
and eid_str not in self._cache_rfc822
|
||||
):
|
||||
missing_ids.append(eid_str)
|
||||
elif "TEXT" in parts or parts == "(BODY[1])":
|
||||
if (
|
||||
eid_str not in self._cache_text
|
||||
and eid_str not in self._cache_rfc822
|
||||
):
|
||||
missing_ids.append(eid_str)
|
||||
else:
|
||||
missing_ids.append(eid_str)
|
||||
|
||||
# Batch fetch only what is missing
|
||||
if missing_ids:
|
||||
# Just use fetch() internally for now to populate individual items since fetch_batch
|
||||
# returns combined lines which might be tricky to parse.
|
||||
# Wait, email_fetch_batch might be tricky to cache without parsing the response to separate emails.
|
||||
# So actually we will just loop `fetch` for batch if we want it cleanly cached, or we don't cache
|
||||
# `fetch_batch` if we aren't parsing it.
|
||||
# Actually `fetch_batch` isn't needed if we just do individual fetches? No, batch is faster.
|
||||
# But parsing batch response to stick into cache is annoying.
|
||||
pass
|
||||
|
||||
# For this version, let's keep fetch_batch simple and bypass cache.
|
||||
return await email_fetch_batch(self.account, email_ids, parts)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the cache."""
|
||||
self._cache_rfc822.clear()
|
||||
self._cache_text.clear()
|
||||
self._cache_headers.clear()
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Date and time helpers for Mail and Packages."""
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
def get_today() -> datetime.date:
|
||||
"""Get today's date using system local timezone (Home Assistant's timezone).
|
||||
|
||||
Returns date object using the system's local timezone.
|
||||
"""
|
||||
return datetime.date.today()
|
||||
|
||||
|
||||
def get_formatted_date() -> str:
|
||||
"""Return today in specific format.
|
||||
|
||||
Returns current timestamp as string
|
||||
"""
|
||||
return get_today().strftime("%d-%b-%Y")
|
||||
|
||||
|
||||
async def update_time() -> datetime.datetime:
|
||||
"""Get update time.
|
||||
|
||||
Returns current timestamp as datetime object.
|
||||
"""
|
||||
return datetime.datetime.now(datetime.UTC)
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Email parsing and validation utilities for Mail and Packages."""
|
||||
|
||||
import email
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from aioimaplib import IMAP4_SSL
|
||||
from voluptuous import Email, MultipleInvalid, Schema
|
||||
|
||||
from custom_components.mail_and_packages.const import SENSOR_DATA
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
|
||||
from .imap import email_fetch
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_email_address(email_address: str) -> bool:
|
||||
"""Validate the format of an email address.
|
||||
|
||||
Args:
|
||||
email_address (str): The email address to validate.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the email address is valid, `False` otherwise.
|
||||
|
||||
"""
|
||||
try:
|
||||
schema = Schema(Email()) # pylint: disable=no-value-for-parameter
|
||||
schema(email_address)
|
||||
except MultipleInvalid:
|
||||
_LOGGER.error("'%s' does not look like a valid email address", email_address)
|
||||
return False
|
||||
|
||||
_LOGGER.debug("%s is a valid email address", email_address)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def generate_service_email_domains(amazon_fwds: list) -> set[str]:
|
||||
"""Generate a set of service email domains from amazon domains and SENSOR_DATA.
|
||||
|
||||
Returns:
|
||||
set[str]: A set of unique email domains.
|
||||
|
||||
"""
|
||||
domains = {fwd.split("@")[1] for fwd in amazon_fwds if "@" in fwd}
|
||||
for sensor in SENSOR_DATA.values():
|
||||
for address in sensor.get("email", []):
|
||||
if "@" not in address:
|
||||
continue
|
||||
domains.add(address.split("@")[1])
|
||||
return domains
|
||||
|
||||
|
||||
def _match_patterns(
|
||||
text: str,
|
||||
patterns: list[re.Pattern],
|
||||
body_count: bool,
|
||||
) -> tuple[int, int | None]:
|
||||
"""Apply patterns to text and return occurrence count and extracted value.
|
||||
|
||||
Returns:
|
||||
tuple[int, int | None]: (count_of_matches, extracted_value)
|
||||
|
||||
"""
|
||||
local_count = 0
|
||||
extracted_value = None
|
||||
|
||||
for pattern in patterns:
|
||||
if body_count:
|
||||
if (found := pattern.search(text)) and len(found.groups()) > 0:
|
||||
_LOGGER.debug(
|
||||
"Found (%s) in email result: %s",
|
||||
pattern.pattern,
|
||||
found.groups(),
|
||||
)
|
||||
extracted_value = int(found.group(1))
|
||||
elif (found := pattern.findall(text)) and len(found) > 0:
|
||||
_LOGGER.debug(
|
||||
"Found (%s) in email %s times.",
|
||||
pattern.pattern,
|
||||
len(found),
|
||||
)
|
||||
local_count += len(found)
|
||||
|
||||
return local_count, extracted_value
|
||||
|
||||
|
||||
async def _scan_email_for_text(
|
||||
account: type[IMAP4_SSL],
|
||||
email_id: str,
|
||||
patterns: list[re.Pattern],
|
||||
body_count: bool,
|
||||
cache: EmailCache | None = None,
|
||||
) -> tuple[int, int | None]:
|
||||
"""Scan a single email for terms.
|
||||
|
||||
Returns:
|
||||
tuple[int, int | None]: (total_matches, last_extracted_value)
|
||||
|
||||
"""
|
||||
total_matches = 0
|
||||
last_value = None
|
||||
|
||||
if cache:
|
||||
data = (await cache.fetch(email_id, "(RFC822)"))[1]
|
||||
else:
|
||||
data = (await email_fetch(account, email_id, "(RFC822)"))[1]
|
||||
for response_part in data:
|
||||
if not isinstance(response_part, (bytes, bytearray)):
|
||||
continue
|
||||
|
||||
msg = email.message_from_bytes(response_part)
|
||||
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() not in ["text/html", "text/plain"]:
|
||||
continue
|
||||
|
||||
email_msg = part.get_payload(decode=True)
|
||||
try:
|
||||
email_msg = email_msg.decode("utf-8", "ignore")
|
||||
except (AttributeError, UnicodeError):
|
||||
continue
|
||||
|
||||
matches, value = _match_patterns(email_msg, patterns, body_count)
|
||||
total_matches += matches
|
||||
if value is not None:
|
||||
last_value = value
|
||||
|
||||
return total_matches, last_value
|
||||
|
||||
|
||||
async def find_text(
|
||||
sdata: Any,
|
||||
account: type[IMAP4_SSL],
|
||||
search_terms: list,
|
||||
body_count: bool,
|
||||
cache: EmailCache | None = None,
|
||||
) -> int:
|
||||
"""Filter for specific words in email."""
|
||||
_LOGGER.debug("Searching for (%s) in (%s) emails", search_terms, len(sdata))
|
||||
mail_list = sdata[0].split()
|
||||
count = 0
|
||||
|
||||
# Pre-compile regex patterns once
|
||||
patterns = [re.compile(rf"{term}") for term in search_terms]
|
||||
|
||||
for i in mail_list:
|
||||
matches, value = await _scan_email_for_text(
|
||||
account, i, patterns, body_count, cache
|
||||
)
|
||||
|
||||
if body_count:
|
||||
# If extracting a value, "last found value wins" (updates count)
|
||||
if value is not None:
|
||||
count = value
|
||||
else:
|
||||
# If counting occurrences, accumulate
|
||||
count += matches
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def find_text_matches(
|
||||
sdata: Any,
|
||||
account: type[IMAP4_SSL],
|
||||
search_terms: list,
|
||||
body_count: bool,
|
||||
cache: EmailCache | None = None,
|
||||
) -> tuple[int, list[bytes]]:
|
||||
"""Filter for specific words in email and return matches and matching IDs."""
|
||||
_LOGGER.debug("Searching for (%s) in (%s) emails", search_terms, len(sdata))
|
||||
mail_list = sdata[0].split()
|
||||
count = 0
|
||||
matching_ids = []
|
||||
|
||||
# Pre-compile regex patterns once
|
||||
patterns = [re.compile(rf"{term}") for term in search_terms]
|
||||
|
||||
for i in mail_list:
|
||||
matches, value = await _scan_email_for_text(
|
||||
account, i, patterns, body_count, cache
|
||||
)
|
||||
|
||||
matched = False
|
||||
if body_count:
|
||||
# If extracting a value, "last found value wins" (updates count)
|
||||
if value is not None:
|
||||
count = value
|
||||
matched = True
|
||||
# If counting occurrences, accumulate
|
||||
elif matches > 0:
|
||||
count += matches
|
||||
matched = True
|
||||
|
||||
if matched:
|
||||
matching_ids.append(i)
|
||||
|
||||
return count, matching_ids
|
||||
@@ -0,0 +1,485 @@
|
||||
"""Image processing and management utilities for Mail and Packages."""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import subprocess # nosec
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from shutil import copyfile, which
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
from custom_components.mail_and_packages.const import (
|
||||
CONF_AMAZON_CUSTOM_IMG,
|
||||
CONF_AMAZON_CUSTOM_IMG_FILE,
|
||||
CONF_CUSTOM_IMG,
|
||||
CONF_CUSTOM_IMG_FILE,
|
||||
CONF_FEDEX_CUSTOM_IMG,
|
||||
CONF_FEDEX_CUSTOM_IMG_FILE,
|
||||
CONF_STORAGE,
|
||||
CONF_UPS_CUSTOM_IMG,
|
||||
CONF_UPS_CUSTOM_IMG_FILE,
|
||||
CONF_WALMART_CUSTOM_IMG,
|
||||
CONF_WALMART_CUSTOM_IMG_FILE,
|
||||
DEFAULT_AMAZON_CUSTOM_IMG_FILE,
|
||||
DEFAULT_CUSTOM_IMG_FILE,
|
||||
DEFAULT_FEDEX_CUSTOM_IMG_FILE,
|
||||
DEFAULT_UPS_CUSTOM_IMG_FILE,
|
||||
DEFAULT_WALMART_CUSTOM_IMG_FILE,
|
||||
OVERLAY,
|
||||
)
|
||||
|
||||
from .date import get_formatted_date
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _check_ffmpeg() -> bool:
|
||||
"""Check if ffmpeg is installed.
|
||||
|
||||
Returns boolean
|
||||
"""
|
||||
return which("ffmpeg")
|
||||
|
||||
|
||||
def default_image_path(
|
||||
hass: HomeAssistant, # pylint: disable=unused-argument
|
||||
config_entry: ConfigEntry,
|
||||
) -> str:
|
||||
"""Return value of the default image path.
|
||||
|
||||
Returns the default path based on logic
|
||||
"""
|
||||
storage = None
|
||||
try:
|
||||
storage = config_entry.get(CONF_STORAGE)
|
||||
except AttributeError:
|
||||
storage = config_entry.data[CONF_STORAGE]
|
||||
|
||||
if storage:
|
||||
return storage
|
||||
return "custom_components/mail_and_packages/images/"
|
||||
|
||||
|
||||
def hash_file(filename: str) -> str:
|
||||
"""Return the SHA-1 hash of the file passed into it.
|
||||
|
||||
Returns hash of file as string
|
||||
"""
|
||||
# make a hash object
|
||||
the_hash = hashlib.sha1() # nosec
|
||||
|
||||
# open file for reading in binary mode
|
||||
with Path(filename).open("rb") as file:
|
||||
# loop till the end of the file
|
||||
chunk = 0
|
||||
while chunk != b"":
|
||||
# read only 1024 bytes at a time
|
||||
chunk = file.read(1024)
|
||||
the_hash.update(chunk)
|
||||
|
||||
# return the hex representation of digest
|
||||
return the_hash.hexdigest()
|
||||
|
||||
|
||||
def _delete_file(file_path: Path, context: str) -> bool:
|
||||
"""Delete a single file with error handling."""
|
||||
try:
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
_LOGGER.debug("%s - Successfully removed: %s", context, file_path)
|
||||
return True
|
||||
_LOGGER.debug("%s - File does not exist: %s", context, file_path)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error attempting to remove image in %s: %s", context, err)
|
||||
return False
|
||||
|
||||
|
||||
def _cleanup_directory(path: str) -> None:
|
||||
"""Handle directory-wide cleanup."""
|
||||
if not Path(path).is_dir():
|
||||
_LOGGER.debug("cleanup_images - Directory does not exist: %s", path)
|
||||
return
|
||||
|
||||
try:
|
||||
files_before = [x.name for x in Path(path).iterdir()]
|
||||
_LOGGER.debug(
|
||||
"cleanup_images - Files in directory BEFORE cleanup: %s",
|
||||
files_before,
|
||||
)
|
||||
for file in files_before:
|
||||
if file.endswith((".gif", ".mp4", ".jpg", ".png")):
|
||||
full_path = Path(path) / file
|
||||
_delete_file(full_path, "cleanup_images")
|
||||
|
||||
files_after = []
|
||||
if Path(path).is_dir():
|
||||
files_after = [f.name for f in Path(path).iterdir()]
|
||||
|
||||
_LOGGER.debug(
|
||||
"cleanup_images - Files in directory AFTER cleanup: %s",
|
||||
files_after,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.debug("cleanup_images - Directory removed during cleanup: %s", path)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error listing directory for cleanup: %s", err)
|
||||
|
||||
|
||||
def cleanup_images(path: str, image: str | None = None) -> None:
|
||||
"""Clean up image storage directory.
|
||||
|
||||
Only suppose to delete .gif, .mp4, and .jpg files
|
||||
"""
|
||||
_LOGGER.debug("=== cleanup_images CALLED === path: %s, image: %s", path, image)
|
||||
|
||||
if isinstance(path, tuple):
|
||||
path, image = path
|
||||
|
||||
if image is not None:
|
||||
full_path = Path(path) / image
|
||||
_delete_file(full_path, "cleanup_images")
|
||||
return
|
||||
|
||||
_cleanup_directory(path)
|
||||
|
||||
|
||||
def copy_overlays(path: str) -> None:
|
||||
"""Copy overlay images to image output path."""
|
||||
overlays = OVERLAY
|
||||
check = all(item.name in overlays for item in Path(path).iterdir())
|
||||
|
||||
# Copy files if they are missing
|
||||
if not check:
|
||||
for file in overlays:
|
||||
_LOGGER.debug("Copying file to: %s", path + file)
|
||||
copyfile(
|
||||
Path(__file__).parent.parent / file,
|
||||
path + file,
|
||||
)
|
||||
|
||||
|
||||
def resize_images(images: list, width: int, height: int) -> list:
|
||||
"""Resize images."""
|
||||
all_images = []
|
||||
for image_path in images:
|
||||
try:
|
||||
img_path = Path(image_path)
|
||||
with img_path.open("rb") as fd_img:
|
||||
img = Image.open(fd_img)
|
||||
img.thumbnail((width, height), resample=Image.Resampling.LANCZOS)
|
||||
img = ImageOps.pad(
|
||||
img,
|
||||
(width, height),
|
||||
method=Image.Resampling.LANCZOS,
|
||||
)
|
||||
img = img.crop((0, 0, width, height))
|
||||
new_image_path = img_path.parent / f"{img_path.stem}_resized.gif"
|
||||
img.save(new_image_path, "GIF")
|
||||
all_images.append(str(new_image_path))
|
||||
|
||||
except (OSError, ValueError) as err:
|
||||
_LOGGER.error("Error processing image %s: %s", image_path, err)
|
||||
continue
|
||||
|
||||
return all_images
|
||||
|
||||
|
||||
def random_filename(ext: str = ".jpg") -> str:
|
||||
"""Generate random filename."""
|
||||
return f"{uuid.uuid4()!s}{ext}"
|
||||
|
||||
|
||||
def io_save_file(path: str | Path, data: bytes) -> None:
|
||||
"""Write bytes to a file synchronously (for use in executor)."""
|
||||
with Path(path).open("wb") as the_file:
|
||||
the_file.write(data)
|
||||
|
||||
|
||||
def _generate_mp4(path: str, image_file: str) -> None:
|
||||
"""Generate mp4 from gif.
|
||||
|
||||
use a subprocess so we don't lock up the thread
|
||||
command: ffmpeg -f gif -i infile.gif outfile.mp4
|
||||
"""
|
||||
base_path = Path(path)
|
||||
gif_image = base_path / image_file
|
||||
mp4_file = base_path / image_file.replace(".gif", ".mp4")
|
||||
|
||||
filecheck = mp4_file.is_file()
|
||||
|
||||
_LOGGER.debug("Generating mp4: %s", mp4_file)
|
||||
if filecheck:
|
||||
# Construct path string with trailing slash to ensure cleanup_images concatenates correctly
|
||||
cleanup_images(str(mp4_file.parent) + "/", mp4_file.name)
|
||||
_LOGGER.debug("Removing old mp4: %s", mp4_file)
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-i",
|
||||
str(gif_image),
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
str(mp4_file),
|
||||
]
|
||||
subprocess.run(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as err:
|
||||
_LOGGER.error("FFmpeg failed to generate MP4: %s", err)
|
||||
|
||||
|
||||
def generate_grid_img(path: str, image_file: str, count: int) -> None:
|
||||
"""Generate png grid from gif.
|
||||
|
||||
use a subprocess so we don't lock up the thread
|
||||
command: ffmpeg -f gif -i infile.gif outfile.mp4
|
||||
"""
|
||||
count = max(count, 1)
|
||||
if count % 2 == 0:
|
||||
length = int(count / 2)
|
||||
else:
|
||||
length = int(count / 2) + count % 2
|
||||
|
||||
gif_image = Path(path + image_file)
|
||||
png_file = image_file.replace(".gif", "_grid.png")
|
||||
png_image = Path(path).joinpath(png_file)
|
||||
|
||||
filecheck = png_image.is_file()
|
||||
|
||||
_LOGGER.debug("Generating png image grid %s from %s", png_image, gif_image)
|
||||
if filecheck:
|
||||
# cleanup_images expects a tuple or string path, so we use string parts here
|
||||
# or we could update cleanup_images to handle Path objects natively later.
|
||||
cleanup_images(str(png_image.parent) + "/", png_image.name)
|
||||
_LOGGER.debug("Removing old png grid: %s", png_image)
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
str(gif_image),
|
||||
"-r",
|
||||
"0.20",
|
||||
"-filter_complex",
|
||||
f"tile=2x{length}:padding=10:color=black",
|
||||
str(png_image),
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as err:
|
||||
_LOGGER.error("FFmpeg failed to generate grid image: %s", err)
|
||||
|
||||
|
||||
def generate_delivery_gif(
|
||||
delivery_images: list,
|
||||
gif_path: str,
|
||||
duration: int = 3000,
|
||||
) -> bool:
|
||||
"""Generate an animated GIF from delivery images.
|
||||
|
||||
Args:
|
||||
delivery_images: List of image file paths
|
||||
gif_path: Path where the GIF should be saved
|
||||
duration: Duration for each frame in milliseconds (default: 3000)
|
||||
|
||||
Returns:
|
||||
bool: True if GIF was created successfully, False otherwise
|
||||
|
||||
"""
|
||||
try:
|
||||
# Open all images
|
||||
corrected_images = []
|
||||
for img_path in delivery_images:
|
||||
img = Image.open(img_path)
|
||||
img = ImageOps.exif_transpose(img) # auto-rotates according to EXIF
|
||||
corrected_images.append(img)
|
||||
|
||||
# Create animated GIF
|
||||
corrected_images[0].save(
|
||||
gif_path,
|
||||
format="GIF",
|
||||
append_images=corrected_images[1:],
|
||||
save_all=True,
|
||||
duration=duration,
|
||||
loop=0, # Infinite loop
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Generated animated GIF with %d delivery images at %s",
|
||||
len(delivery_images),
|
||||
gif_path,
|
||||
)
|
||||
|
||||
except (OSError, ValueError, Image.UnidentifiedImageError) as e:
|
||||
_LOGGER.error("Error creating animated GIF: %s", e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _get_courier_info(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
amazon: bool,
|
||||
ups: bool,
|
||||
walmart: bool,
|
||||
fedex: bool,
|
||||
) -> tuple[str, str]:
|
||||
"""Determine the path and default image for the active courier."""
|
||||
configs = [
|
||||
(
|
||||
amazon,
|
||||
CONF_AMAZON_CUSTOM_IMG,
|
||||
CONF_AMAZON_CUSTOM_IMG_FILE,
|
||||
DEFAULT_AMAZON_CUSTOM_IMG_FILE,
|
||||
"no_deliveries_amazon.jpg",
|
||||
"amazon",
|
||||
),
|
||||
(
|
||||
ups,
|
||||
CONF_UPS_CUSTOM_IMG,
|
||||
CONF_UPS_CUSTOM_IMG_FILE,
|
||||
DEFAULT_UPS_CUSTOM_IMG_FILE,
|
||||
"no_deliveries_ups.jpg",
|
||||
"ups",
|
||||
),
|
||||
(
|
||||
walmart,
|
||||
CONF_WALMART_CUSTOM_IMG,
|
||||
CONF_WALMART_CUSTOM_IMG_FILE,
|
||||
DEFAULT_WALMART_CUSTOM_IMG_FILE,
|
||||
"no_deliveries_walmart.jpg",
|
||||
"walmart",
|
||||
),
|
||||
(
|
||||
fedex,
|
||||
CONF_FEDEX_CUSTOM_IMG,
|
||||
CONF_FEDEX_CUSTOM_IMG_FILE,
|
||||
DEFAULT_FEDEX_CUSTOM_IMG_FILE,
|
||||
"no_deliveries_fedex.jpg",
|
||||
"fedex",
|
||||
),
|
||||
]
|
||||
|
||||
base_path = f"{hass.config.path()}/{default_image_path(hass, config)}"
|
||||
|
||||
for (
|
||||
active,
|
||||
img_conf,
|
||||
file_conf,
|
||||
default_file_conf,
|
||||
local_default,
|
||||
sub_dir,
|
||||
) in configs:
|
||||
if active:
|
||||
_LOGGER.debug("Processing %s image file name", sub_dir.title())
|
||||
if config.get(img_conf):
|
||||
mail_none = config.get(file_conf) or default_file_conf
|
||||
_LOGGER.debug("Using custom %s image: %s", sub_dir.title(), mail_none)
|
||||
else:
|
||||
mail_none = str(Path(__file__).parent.parent / local_default)
|
||||
_LOGGER.debug("Using default %s image: %s", sub_dir.title(), mail_none)
|
||||
return f"{base_path}{sub_dir}", mail_none
|
||||
|
||||
# Standard mail case
|
||||
path = base_path.rstrip("/")
|
||||
if config.get(CONF_CUSTOM_IMG):
|
||||
mail_none = config.get(CONF_CUSTOM_IMG_FILE) or DEFAULT_CUSTOM_IMG_FILE
|
||||
else:
|
||||
mail_none = str(Path(__file__).parent.parent / "mail_none.gif")
|
||||
return path, mail_none
|
||||
|
||||
|
||||
def _get_image_name_from_directory(
|
||||
path: str, mail_none: str, sha1: str, ext: str
|
||||
) -> str:
|
||||
"""Check existing images and return a filename."""
|
||||
image_name = os.path.split(mail_none)[1]
|
||||
today = get_formatted_date()
|
||||
|
||||
try:
|
||||
for file_path in Path(path).iterdir():
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
filename = file_path.name
|
||||
if filename == "generic_deliveries.gif":
|
||||
continue
|
||||
is_image_file = filename.endswith(".gif") or (
|
||||
filename.endswith(".jpg") and ext == ".jpg"
|
||||
)
|
||||
if is_image_file:
|
||||
try:
|
||||
created = datetime.datetime.fromtimestamp(
|
||||
file_path.stat().st_ctime,
|
||||
).strftime("%d-%b-%Y")
|
||||
# If it's the correct hash OR created today, we can reuse it
|
||||
if sha1 == hash_file(str(file_path)) or today == created:
|
||||
image_name = filename
|
||||
break
|
||||
image_name = f"{uuid.uuid4()!s}{ext}"
|
||||
except OSError as err:
|
||||
_LOGGER.error("Problem accessing file %s: %s", filename, err)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error accessing directory %s: %s", path, err)
|
||||
|
||||
if image_name in mail_none:
|
||||
image_name = f"{uuid.uuid4()!s}{ext}"
|
||||
_LOGGER.debug("=== image_file_name GENERATED NEW UUID: %s ===", image_name)
|
||||
else:
|
||||
_LOGGER.debug("=== image_file_name USING EXISTING: %s ===", image_name)
|
||||
|
||||
return image_name
|
||||
|
||||
|
||||
def image_file_name(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
amazon: bool = False,
|
||||
ups: bool = False,
|
||||
walmart: bool = False,
|
||||
fedex: bool = False,
|
||||
) -> str:
|
||||
"""Determine if filename is to be changed or not.
|
||||
|
||||
Returns filename
|
||||
"""
|
||||
_LOGGER.debug(
|
||||
"=== image_file_name CALLED === - amazon: %s, ups: %s, walmart: %s, fedex: %s",
|
||||
amazon,
|
||||
ups,
|
||||
walmart,
|
||||
fedex,
|
||||
)
|
||||
|
||||
path, mail_none = _get_courier_info(hass, config, amazon, ups, walmart, fedex)
|
||||
image_name = os.path.split(mail_none)[1]
|
||||
|
||||
# Path check
|
||||
try:
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error creating directory: %s", err)
|
||||
return image_name
|
||||
|
||||
# SHA1 file hash check
|
||||
try:
|
||||
sha1 = hash_file(mail_none)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Problem accessing file: %s, error returned: %s", mail_none, err)
|
||||
return image_name
|
||||
|
||||
ext = ".jpg" if ups or walmart or fedex else ".gif"
|
||||
return _get_image_name_from_directory(path, mail_none, sha1, ext)
|
||||
@@ -0,0 +1,722 @@
|
||||
"""IMAP connection and search utilities for Mail and Packages."""
|
||||
|
||||
import asyncio
|
||||
import binascii
|
||||
import logging
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
import aioimaplib
|
||||
from aioimaplib import (
|
||||
AUTH,
|
||||
IMAP4,
|
||||
IMAP4_SSL,
|
||||
NONAUTH,
|
||||
SELECTED,
|
||||
AioImapException,
|
||||
Cmd,
|
||||
Command,
|
||||
Exec,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import ssl
|
||||
|
||||
from custom_components.mail_and_packages.const import DEFAULT_IMAP_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Register ESEARCH command if not already present in aioimaplib
|
||||
if "ESEARCH" not in aioimaplib.Commands:
|
||||
aioimaplib.Commands["ESEARCH"] = Cmd("ESEARCH", (AUTH, SELECTED), Exec.is_async)
|
||||
|
||||
|
||||
def encode_imap_utf7(s: str) -> str:
|
||||
"""Encode a string into IMAP modified UTF-7."""
|
||||
res = []
|
||||
unicode_buffer = []
|
||||
|
||||
def flush_unicode():
|
||||
if unicode_buffer:
|
||||
u_str = "".join(unicode_buffer)
|
||||
encoded_bytes = u_str.encode("utf-16be")
|
||||
b64 = (
|
||||
binascii.b2a_base64(encoded_bytes)
|
||||
.decode("ascii")
|
||||
.rstrip("\n=")
|
||||
.replace("/", ",")
|
||||
)
|
||||
res.append(f"&{b64}-")
|
||||
unicode_buffer.clear()
|
||||
|
||||
for char in s:
|
||||
ord_c = ord(char)
|
||||
if 0x20 <= ord_c <= 0x7E:
|
||||
if char == "&":
|
||||
flush_unicode()
|
||||
res.append("&-")
|
||||
else:
|
||||
if unicode_buffer:
|
||||
flush_unicode()
|
||||
res.append(char)
|
||||
else:
|
||||
unicode_buffer.append(char)
|
||||
|
||||
flush_unicode()
|
||||
return "".join(res)
|
||||
|
||||
|
||||
def decode_imap_utf7(s: str) -> str:
|
||||
"""Decode a string from IMAP modified UTF-7."""
|
||||
res = []
|
||||
i = 0
|
||||
n = len(s)
|
||||
while i < n:
|
||||
char = s[i]
|
||||
if char == "&":
|
||||
end = s.find("-", i + 1)
|
||||
if end == -1:
|
||||
res.append("&")
|
||||
i += 1
|
||||
elif end == i + 1:
|
||||
res.append("&")
|
||||
i += 2
|
||||
else:
|
||||
b64_part = s[i + 1 : end]
|
||||
b64_part = b64_part.replace(",", "/")
|
||||
pad = len(b64_part) % 4
|
||||
if pad:
|
||||
b64_part += "=" * (4 - pad)
|
||||
try:
|
||||
decoded_bytes = binascii.a2b_base64(b64_part)
|
||||
res.append(decoded_bytes.decode("utf-16be"))
|
||||
except (binascii.Error, UnicodeDecodeError, ValueError):
|
||||
res.append(s[i : end + 1])
|
||||
i = end + 1
|
||||
else:
|
||||
res.append(char)
|
||||
i += 1
|
||||
|
||||
return "".join(res)
|
||||
|
||||
|
||||
_ATOM_SPECIALS = frozenset('(){%*"\\] ')
|
||||
|
||||
|
||||
def _is_imap_atom(s: str) -> bool:
|
||||
"""Check if the string is a valid IMAP atom."""
|
||||
return bool(s) and all(0x20 < ord(c) < 0x7F and c not in _ATOM_SPECIALS for c in s)
|
||||
|
||||
|
||||
def quote_folder(folder: str) -> str:
|
||||
"""Ensure folder name is properly quoted for IMAP commands."""
|
||||
if folder.startswith('"') and folder.endswith('"'):
|
||||
return folder
|
||||
return folder if _is_imap_atom(folder) else f'"{folder}"'
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Raise exception for invalid credentials."""
|
||||
|
||||
|
||||
async def login(
|
||||
hass: HomeAssistant,
|
||||
host: str,
|
||||
port: int,
|
||||
user: str,
|
||||
pwd: str,
|
||||
security: str,
|
||||
verify: bool = True,
|
||||
oauth_token: str | None = None,
|
||||
timeout: float = DEFAULT_IMAP_TIMEOUT,
|
||||
) -> IMAP4_SSL | IMAP4:
|
||||
"""Login to IMAP server asynchronously.
|
||||
|
||||
Supports both password and OAuth2 (XOAUTH2) authentication.
|
||||
If oauth_token is provided, uses XOAUTH2 SASL mechanism.
|
||||
Otherwise falls back to standard LOGIN command.
|
||||
"""
|
||||
ssl_context = (
|
||||
ssl.client_context(ssl.SSLCipherList.PYTHON_DEFAULT)
|
||||
if verify
|
||||
else ssl.create_no_verify_ssl_context()
|
||||
)
|
||||
if security == "SSL":
|
||||
account = IMAP4_SSL(
|
||||
host=host, port=port, ssl_context=ssl_context, timeout=timeout
|
||||
)
|
||||
else:
|
||||
account = IMAP4(host=host, port=port, timeout=timeout)
|
||||
|
||||
await account.wait_hello_from_server()
|
||||
|
||||
if account.protocol.state == NONAUTH:
|
||||
try:
|
||||
if oauth_token:
|
||||
await account.xoauth2(user, oauth_token)
|
||||
else:
|
||||
await account.login(user, pwd)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error logging in to IMAP Server: %s", err)
|
||||
raise InvalidAuth from err
|
||||
|
||||
if account.protocol.state not in {AUTH, SELECTED}:
|
||||
_LOGGER.error("Error logging in to IMAP Server")
|
||||
raise InvalidAuth
|
||||
return account
|
||||
|
||||
|
||||
async def selectfolder(account: IMAP4_SSL, folder: str) -> bool:
|
||||
"""Select folder inside the mailbox asynchronously."""
|
||||
if getattr(account, "_current_folder", None) == folder:
|
||||
return True
|
||||
|
||||
encoded_folder = encode_imap_utf7(folder)
|
||||
quoted_folder = quote_folder(encoded_folder)
|
||||
|
||||
try:
|
||||
await account.select(quoted_folder)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error selecting folder %s: %s", folder, err)
|
||||
return False
|
||||
else:
|
||||
account._current_folder = folder # noqa: SLF001
|
||||
return True
|
||||
|
||||
|
||||
def clean_search_string(val: str) -> str:
|
||||
"""Clean search string for IMAP search compatibility.
|
||||
|
||||
Normalizes Unicode characters to NFKD decomposed form, strips non-ASCII
|
||||
characters to ensure compatibility with US-ASCII only IMAP servers,
|
||||
and removes any double quotes to prevent syntax corruption.
|
||||
"""
|
||||
if not val:
|
||||
return ""
|
||||
normalized = unicodedata.normalize("NFKD", val)
|
||||
cleaned = normalized.encode("ascii", "ignore").decode("ascii")
|
||||
return cleaned.replace('"', "")
|
||||
|
||||
|
||||
def build_search( # noqa: C901
|
||||
address: list,
|
||||
date: str,
|
||||
subject: str | list[str] = "",
|
||||
body: str | list[str] = "",
|
||||
header: str = "",
|
||||
is_yahoo: bool = False,
|
||||
) -> tuple:
|
||||
"""Build IMAP search query.
|
||||
|
||||
Return tuple of utf8 flag and search query.
|
||||
Non-ASCII characters are stripped from subject to ensure compatibility
|
||||
with servers that only support US-ASCII charset (e.g. Microsoft Exchange).
|
||||
IMAP SUBJECT performs substring matching, so stripping non-ASCII chars
|
||||
still matches the original subject (e.g. 'Livr' matches 'Livré').
|
||||
|
||||
When `header` is provided, each address is matched as either a forwarded
|
||||
email (via HEADER substring match) OR a direct email (via FROM), so the
|
||||
same config works for carriers that are forwarded through a service like
|
||||
SimpleLogin AND carriers whose emails arrive directly in the mailbox.
|
||||
IMAP HEADER does substring matching, so "mcinfo@ups.com" will match a
|
||||
header value of "UPS <mcinfo@ups.com>".
|
||||
"""
|
||||
the_date = f"SINCE {date}"
|
||||
|
||||
if not address:
|
||||
raise ValueError("address list must not be empty")
|
||||
|
||||
# Build the address/header clause
|
||||
if header:
|
||||
# Each address matches via header (forwarded) OR FROM (direct), so
|
||||
# users with mixed setups (some carriers forwarded, others direct)
|
||||
# don't need separate configurations.
|
||||
parts = [f'OR HEADER "{header}" "{a}" FROM "{a}"' for a in address]
|
||||
if len(parts) == 1:
|
||||
addr_clause = f"({parts[0]})" if is_yahoo else parts[0]
|
||||
else:
|
||||
or_prefix = " ".join(["OR"] * (len(parts) - 1))
|
||||
addr_clause = (
|
||||
f"({or_prefix} {' '.join(parts)})"
|
||||
if is_yahoo
|
||||
else f"{or_prefix} {' '.join(parts)}"
|
||||
)
|
||||
elif len(address) == 1:
|
||||
addr_clause = f'FROM "{address[0]}"'
|
||||
else:
|
||||
joined = '" FROM "'.join(address)
|
||||
or_prefix = " ".join(["OR"] * (len(address) - 1))
|
||||
addr_clause = (
|
||||
f'({or_prefix} FROM "{joined}")'
|
||||
if is_yahoo
|
||||
else f'{or_prefix} FROM "{joined}"'
|
||||
)
|
||||
|
||||
# Handle multiple subjects
|
||||
subject_part = ""
|
||||
if subject:
|
||||
subjects = [subject] if isinstance(subject, str) else subject
|
||||
safe_subjects = [clean_search_string(s) for s in subjects]
|
||||
safe_subjects = [s for s in safe_subjects if s]
|
||||
|
||||
if len(safe_subjects) == 1:
|
||||
subject_part = f'SUBJECT "{safe_subjects[0]}"'
|
||||
elif len(safe_subjects) > 1:
|
||||
subject_prefix = " ".join(["OR"] * (len(safe_subjects) - 1))
|
||||
subject_joined = '" SUBJECT "'.join(safe_subjects)
|
||||
subject_part = (
|
||||
f'({subject_prefix} SUBJECT "{subject_joined}")'
|
||||
if is_yahoo
|
||||
else f'{subject_prefix} SUBJECT "{subject_joined}"'
|
||||
)
|
||||
|
||||
# Handle multiple bodies
|
||||
body_part = ""
|
||||
if body:
|
||||
bodies = [body] if isinstance(body, str) else body
|
||||
safe_bodies = [clean_search_string(b) for b in bodies]
|
||||
safe_bodies = [b for b in safe_bodies if b]
|
||||
|
||||
if len(safe_bodies) == 1:
|
||||
body_part = f'BODY "{safe_bodies[0]}"'
|
||||
elif len(safe_bodies) > 1:
|
||||
body_prefix = " ".join(["OR"] * (len(safe_bodies) - 1))
|
||||
body_joined = '" BODY "'.join(safe_bodies)
|
||||
body_part = (
|
||||
f'({body_prefix} BODY "{body_joined}")'
|
||||
if is_yahoo
|
||||
else f'{body_prefix} BODY "{body_joined}"'
|
||||
)
|
||||
|
||||
if is_yahoo:
|
||||
if subject_part or body_part:
|
||||
search_criteria = f"{subject_part} {body_part}".strip()
|
||||
imap_search = f"({addr_clause} {search_criteria} {the_date})"
|
||||
else:
|
||||
imap_search = f"({addr_clause} {the_date})"
|
||||
elif subject_part or body_part:
|
||||
search_criteria = f"{subject_part} {body_part}".strip()
|
||||
imap_search = f"{addr_clause} {search_criteria} {the_date}"
|
||||
else:
|
||||
imap_search = f"{addr_clause} {the_date}"
|
||||
|
||||
_LOGGER.debug("DEBUG imap_search: %s", imap_search)
|
||||
|
||||
return (False, imap_search)
|
||||
|
||||
|
||||
def parse_search_response(lines: list[bytes]) -> list[bytes]:
|
||||
"""Parse IMAP SEARCH response lines and return list of UID/ID bytes.
|
||||
|
||||
Handles both standard server responses (prefixed with b"SEARCH")
|
||||
and mocked test inputs (which often contain raw UIDs directly).
|
||||
Filters out the SEARCH keyword, tagged OK/status responses,
|
||||
and any non-numeric tokens.
|
||||
"""
|
||||
uids = []
|
||||
for line in lines:
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if not parts:
|
||||
continue
|
||||
|
||||
if parts[0] == b"SEARCH":
|
||||
# Check if this is a search result line, e.g. b"SEARCH 1001 1002"
|
||||
# (as opposed to b"SEARCH completed")
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
uids.extend(parts[1:])
|
||||
# Check if this line is just a list of numeric UIDs (mock/test compatibility)
|
||||
# and ignore status/existence responses like b"23 EXISTS"
|
||||
elif all(p.isdigit() for p in parts):
|
||||
uids.extend(parts)
|
||||
|
||||
return uids
|
||||
|
||||
|
||||
def _parse_esearch_line(line_bytes: bytes) -> list[bytes]:
|
||||
"""Parse a single ESEARCH line and return list of formatted UID bytes: b'folder/uid'."""
|
||||
line_str = line_bytes.decode("utf-8", "ignore")
|
||||
|
||||
# Extract the correlator inside parentheses
|
||||
start_paren = line_str.find("(")
|
||||
end_paren = line_str.find(")", start_paren) if start_paren != -1 else -1
|
||||
if start_paren == -1 or end_paren == -1:
|
||||
return []
|
||||
|
||||
correlator = line_str[start_paren + 1 : end_paren]
|
||||
|
||||
# Extract mailbox name (could be quoted or unquoted)
|
||||
mailbox_match = re.search(r'MAILBOX\s+"([^"]+)"', correlator)
|
||||
if not mailbox_match:
|
||||
mailbox_match = re.search(r"MAILBOX\s+(\S+)", correlator)
|
||||
if not mailbox_match:
|
||||
return []
|
||||
mailbox = mailbox_match.group(1)
|
||||
mailbox = decode_imap_utf7(mailbox)
|
||||
|
||||
# Extract the sequence set after 'UID ALL' anywhere in the line
|
||||
seq_match = re.search(r"UID\s+ALL\s+(\S+)", line_str)
|
||||
if not seq_match:
|
||||
return []
|
||||
seq_set = seq_match.group(1)
|
||||
|
||||
uids = []
|
||||
for part in seq_set.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
if ":" in part:
|
||||
try:
|
||||
start_str, end_str = part.split(":", 1)
|
||||
start, end = int(start_str), int(end_str)
|
||||
if start <= end:
|
||||
uids.extend(str(x) for x in range(start, end + 1))
|
||||
else:
|
||||
uids.extend(str(x) for x in range(end, start + 1))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
uids.append(part)
|
||||
return [f"{mailbox}/{uid}".encode() for uid in uids]
|
||||
|
||||
|
||||
async def _execute_single_search(account: IMAP4_SSL, search_query: str) -> list[bytes]: # noqa: C901
|
||||
"""Execute search query. If single folder, use standard search. If multiple, use hybrid ESEARCH/fallback."""
|
||||
folders = getattr(account, "_folders", ["INBOX"])
|
||||
|
||||
if len(folders) <= 1:
|
||||
res = await account.search(search_query, charset=None)
|
||||
if res.result == "OK" and res.lines:
|
||||
return parse_search_response(res.lines)
|
||||
return []
|
||||
|
||||
all_uids = []
|
||||
|
||||
# Check for MULTISEARCH capability safely (handling mock/AsyncMock in tests)
|
||||
is_multisearch = False
|
||||
if hasattr(account, "has_capability"):
|
||||
try:
|
||||
res = account.has_capability("MULTISEARCH")
|
||||
if asyncio.iscoroutine(res):
|
||||
res.close()
|
||||
is_multisearch = False
|
||||
else:
|
||||
is_multisearch = bool(res)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
if is_multisearch:
|
||||
# ESEARCH IN ("folder1" "folder2") query - encode and quote folders
|
||||
folder_list = " ".join([quote_folder(encode_imap_utf7(f)) for f in folders])
|
||||
args = ("IN", f"({folder_list})", search_query)
|
||||
try:
|
||||
timeout = getattr(account, "timeout", None)
|
||||
if not isinstance(timeout, (int, float)):
|
||||
timeout = None
|
||||
res = await account.protocol.execute(
|
||||
Command(
|
||||
"ESEARCH",
|
||||
account.protocol.new_tag(),
|
||||
*args,
|
||||
loop=account.protocol.loop,
|
||||
timeout=timeout,
|
||||
)
|
||||
)
|
||||
if res.result == "OK":
|
||||
for line in res.lines:
|
||||
if line:
|
||||
all_uids.extend(_parse_esearch_line(line))
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error executing ESEARCH: %s", err)
|
||||
else:
|
||||
# Sequential select and search fallback - no limits on configured folders
|
||||
for folder in folders:
|
||||
select_ok = await selectfolder(account, folder)
|
||||
if not select_ok:
|
||||
continue
|
||||
try:
|
||||
res = await account.uid_search(search_query, charset=None)
|
||||
if res.result == "OK" and res.lines:
|
||||
parsed = parse_search_response(res.lines)
|
||||
all_uids.extend(
|
||||
f"{folder}/{uid.decode()}".encode() for uid in parsed
|
||||
)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error searching folder %s: %s", folder, err)
|
||||
|
||||
return all_uids
|
||||
|
||||
|
||||
async def email_search( # noqa: C901
|
||||
account: IMAP4_SSL,
|
||||
address: list,
|
||||
date: str,
|
||||
subject: str | list[str] = "",
|
||||
body: str | list[str] = "",
|
||||
header: str = "",
|
||||
) -> tuple:
|
||||
"""Search emails with from/header, subject, and date asynchronously.
|
||||
|
||||
Always uses charset=None to avoid sending CHARSET in the IMAP SEARCH
|
||||
command, ensuring compatibility with servers like Microsoft Exchange
|
||||
that only support US-ASCII.
|
||||
|
||||
When `header` is provided, searches via HEADER criterion instead of FROM,
|
||||
matching the original sender in forwarding-service headers.
|
||||
|
||||
If multiple subjects are provided, they are searched in batches of 10
|
||||
to keep the search query length safe.
|
||||
"""
|
||||
folders = getattr(account, "_folders", ["INBOX"])
|
||||
is_yahoo = False
|
||||
if hasattr(account, "host") and isinstance(account.host, str):
|
||||
host_lower = account.host.lower()
|
||||
is_yahoo = "yahoo" in host_lower or "aol" in host_lower
|
||||
|
||||
# If there are more than 2 body patterns, do not search them server-side
|
||||
# to prevent slow query execution and timeouts on standard IMAP servers.
|
||||
# Instead, we let the shipper's client-side text filtering handle it.
|
||||
body_search = body
|
||||
if body:
|
||||
bodies = [body] if isinstance(body, str) else body
|
||||
if len(bodies) > 2:
|
||||
body_search = ""
|
||||
|
||||
if len(folders) <= 1:
|
||||
if not isinstance(subject, list) or len(subject) <= 10:
|
||||
_unused, search = build_search(
|
||||
address, date, subject, body_search, header, is_yahoo=is_yahoo
|
||||
)
|
||||
try:
|
||||
res = await account.search(search, charset=None)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error searching emails: %s", err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
parsed = parse_search_response(res.lines)
|
||||
return (res.result, [b" ".join(parsed)])
|
||||
|
||||
# Batch subjects in groups of 10
|
||||
all_matched_ids = []
|
||||
for i in range(0, len(subject), 10):
|
||||
batch = subject[i : i + 10]
|
||||
_unused, search = build_search(
|
||||
address, date, batch, body_search, header, is_yahoo=is_yahoo
|
||||
)
|
||||
try:
|
||||
res = await account.search(search, charset=None)
|
||||
if res.result == "OK" and res.lines:
|
||||
parsed = parse_search_response(res.lines)
|
||||
all_matched_ids.extend(parsed)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error searching emails batch: %s", err)
|
||||
|
||||
# Deduplicate and return in same format as individual search
|
||||
unique_ids = list(dict.fromkeys(all_matched_ids))
|
||||
return ("OK", [b" ".join(unique_ids)])
|
||||
|
||||
# Multi-folder search logic
|
||||
if not isinstance(subject, list) or len(subject) <= 10:
|
||||
_unused, search = build_search(
|
||||
address, date, subject, body_search, header, is_yahoo=is_yahoo
|
||||
)
|
||||
try:
|
||||
uids = await _execute_single_search(account, search)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error searching emails: %s", err)
|
||||
return ("BAD", str(err))
|
||||
return ("OK", [b" ".join(uids)])
|
||||
|
||||
# Batch subjects in groups of 10
|
||||
all_matched_ids = []
|
||||
for i in range(0, len(subject), 10):
|
||||
batch = subject[i : i + 10]
|
||||
_unused, search = build_search(
|
||||
address, date, batch, body_search, header, is_yahoo=is_yahoo
|
||||
)
|
||||
try:
|
||||
uids = await _execute_single_search(account, search)
|
||||
all_matched_ids.extend(uids)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error searching emails batch: %s", err)
|
||||
|
||||
# Deduplicate and return in same format as individual search
|
||||
unique_ids = list(dict.fromkeys(all_matched_ids))
|
||||
return ("OK", [b" ".join(unique_ids)])
|
||||
|
||||
|
||||
async def email_fetch(account: IMAP4_SSL, num, parts: str = "(RFC822)") -> tuple:
|
||||
"""Download specified email for parsing asynchronously."""
|
||||
if account.host == "imap.mail.me.com":
|
||||
parts = "BODY[]"
|
||||
|
||||
num_str = num.decode() if isinstance(num, bytes) else str(num)
|
||||
if "/" in num_str:
|
||||
folder, num_str = num_str.rsplit("/", 1)
|
||||
await selectfolder(account, folder)
|
||||
try:
|
||||
res = await account.uid("FETCH", num_str, parts)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching email %s: %s", num_str, err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
return (res.result, res.lines)
|
||||
|
||||
try:
|
||||
res = await account.fetch(num_str, parts)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching email %s: %s", num_str, err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
return (res.result, res.lines)
|
||||
|
||||
|
||||
async def email_fetch_headers(account: IMAP4_SSL, num) -> tuple:
|
||||
"""Download only the subject header of an email asynchronously."""
|
||||
num_str = num.decode() if isinstance(num, bytes) else str(num)
|
||||
if "/" in num_str:
|
||||
folder, num_str = num_str.rsplit("/", 1)
|
||||
await selectfolder(account, folder)
|
||||
try:
|
||||
res = await account.uid("FETCH", num_str, "(BODY[HEADER.FIELDS (SUBJECT)])")
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching email headers %s: %s", num_str, err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
return (res.result, res.lines)
|
||||
|
||||
try:
|
||||
res = await account.fetch(num_str, "(BODY[HEADER.FIELDS (SUBJECT)])")
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching email headers %s: %s", num_str, err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
return (res.result, res.lines)
|
||||
|
||||
|
||||
async def email_fetch_text(account: IMAP4_SSL, num, parts: str = "(BODY[1])") -> tuple:
|
||||
"""Download the specific part of the email body asynchronously."""
|
||||
if account.host == "imap.mail.me.com":
|
||||
parts = "BODY[]"
|
||||
|
||||
num_str = num.decode() if isinstance(num, bytes) else str(num)
|
||||
if "/" in num_str:
|
||||
folder, num_str = num_str.rsplit("/", 1)
|
||||
await selectfolder(account, folder)
|
||||
try:
|
||||
res = await account.uid("FETCH", num_str, parts)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching email text %s: %s", num_str, err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
return (res.result, res.lines)
|
||||
|
||||
try:
|
||||
res = await account.fetch(num_str, parts)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching email text %s: %s", num_str, err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
return (res.result, res.lines)
|
||||
|
||||
|
||||
async def email_fetch_batch( # noqa: C901
|
||||
account: IMAP4_SSL, nums: list[str | bytes], parts: str = "(RFC822)"
|
||||
) -> tuple:
|
||||
"""Download specified emails for parsing asynchronously in a batch."""
|
||||
if not nums:
|
||||
return ("OK", [])
|
||||
|
||||
if account.host == "imap.mail.me.com":
|
||||
parts = "BODY[]"
|
||||
|
||||
# Check if any ID contains a folder prefix
|
||||
has_folder_prefix = False
|
||||
for num in nums:
|
||||
num_str = num.decode() if isinstance(num, bytes) else str(num)
|
||||
if "/" in num_str:
|
||||
has_folder_prefix = True
|
||||
break
|
||||
|
||||
if not has_folder_prefix:
|
||||
num_strs = [
|
||||
num.decode() if isinstance(num, bytes) else str(num) for num in nums
|
||||
]
|
||||
num_list_str = ",".join(num_strs)
|
||||
try:
|
||||
res = await account.fetch(num_list_str, parts)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching emails batch %s: %s", num_list_str, err)
|
||||
return ("BAD", str(err))
|
||||
else:
|
||||
return (res.result, res.lines)
|
||||
|
||||
# Group nums by their folder prefix
|
||||
folder_to_nums = {}
|
||||
for num in nums:
|
||||
num_str = num.decode() if isinstance(num, bytes) else str(num)
|
||||
if "/" in num_str:
|
||||
folder, actual_num = num_str.rsplit("/", 1)
|
||||
else:
|
||||
folder, actual_num = None, num_str
|
||||
folder_to_nums.setdefault(folder, []).append(actual_num)
|
||||
|
||||
all_results = []
|
||||
overall_result = "OK"
|
||||
|
||||
for folder, folder_nums in folder_to_nums.items():
|
||||
if folder is not None:
|
||||
await selectfolder(account, folder)
|
||||
|
||||
num_list_str = ",".join(folder_nums)
|
||||
try:
|
||||
res = await account.uid("FETCH", num_list_str, parts)
|
||||
if res.result != "OK":
|
||||
overall_result = res.result
|
||||
all_results.extend(res.lines)
|
||||
except TimeoutError:
|
||||
raise
|
||||
except (AioImapException, OSError) as err:
|
||||
_LOGGER.error("Error fetching emails batch %s: %s", num_list_str, err)
|
||||
return ("BAD", str(err))
|
||||
|
||||
return (overall_result, all_results)
|
||||
|
||||
|
||||
async def logout(account: IMAP4_SSL | IMAP4) -> None:
|
||||
"""Logout from IMAP server asynchronously."""
|
||||
try:
|
||||
await account.logout()
|
||||
except (TimeoutError, AioImapException, OSError, asyncio.CancelledError) as err:
|
||||
_LOGGER.debug("Error logging out of IMAP Server: %s", err)
|
||||
@@ -0,0 +1,294 @@
|
||||
"""Shipper utility functions for Mail and Packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import email
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from aioimaplib import IMAP4_SSL
|
||||
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
|
||||
from .imap import email_fetch
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_tracking(
|
||||
sdata: Any,
|
||||
account: type[IMAP4_SSL],
|
||||
the_format: str | None = None,
|
||||
cache: EmailCache | None = None,
|
||||
) -> list:
|
||||
"""Parse tracking numbers from email.
|
||||
|
||||
Returns list of tracking numbers
|
||||
"""
|
||||
tracking = []
|
||||
pattern = re.compile(rf"{the_format}")
|
||||
mail_list = sdata.split()
|
||||
_LOGGER.debug("Searching for tracking numbers in %s messages...", len(mail_list))
|
||||
|
||||
for i in mail_list:
|
||||
if cache:
|
||||
data = (await cache.fetch(i, "(RFC822)"))[1]
|
||||
else:
|
||||
data = (await email_fetch(account, i, "(RFC822)"))[1]
|
||||
|
||||
for response_part in data:
|
||||
if not isinstance(response_part, (bytes, bytearray)):
|
||||
continue
|
||||
|
||||
msg = email.message_from_bytes(response_part)
|
||||
|
||||
# 1. Search subject
|
||||
if found := _find_tracking_in_subject(msg, pattern):
|
||||
if found not in tracking:
|
||||
tracking.append(found)
|
||||
continue
|
||||
|
||||
# 2. Search body
|
||||
if the_format == "1Z?[0-9A-Z]{16}":
|
||||
found = _find_ups_tracking_in_raw(response_part, pattern)
|
||||
else:
|
||||
found = _find_tracking_in_body(msg, pattern, the_format)
|
||||
|
||||
if found and found not in tracking:
|
||||
tracking.append(found)
|
||||
|
||||
return tracking
|
||||
|
||||
|
||||
def _find_tracking_in_subject(
|
||||
msg: email.message.Message,
|
||||
pattern: re.Pattern,
|
||||
) -> str | None:
|
||||
"""Find tracking number in email subject."""
|
||||
email_subject = msg["subject"]
|
||||
if email_subject:
|
||||
email_subject = str(email_subject)
|
||||
if (found := pattern.findall(email_subject)) and len(found) > 0:
|
||||
_LOGGER.debug("Found tracking number in email subject: %s", found[0])
|
||||
return found[0]
|
||||
return None
|
||||
|
||||
|
||||
def _find_ups_tracking_in_raw(
|
||||
response_part: bytes | bytearray,
|
||||
pattern: re.Pattern,
|
||||
) -> str | None:
|
||||
"""UPS specific tracking search in raw email bytes."""
|
||||
try:
|
||||
email_content = str(response_part, "utf-8", errors="ignore")
|
||||
if (found := pattern.findall(email_content)) and len(found) > 0:
|
||||
_LOGGER.debug("Found tracking number in email: %s", found[0])
|
||||
return found[0]
|
||||
except (TypeError, UnicodeError) as err:
|
||||
_LOGGER.debug("Error processing email content: %s", err)
|
||||
return None
|
||||
|
||||
|
||||
def _find_tracking_in_body(
|
||||
msg: email.message.Message,
|
||||
pattern: re.Pattern,
|
||||
the_format: str,
|
||||
) -> str | None:
|
||||
"""Search for tracking number in email body parts."""
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() not in ["text/html", "text/plain"]:
|
||||
continue
|
||||
email_msg = part.get_payload(decode=True)
|
||||
email_msg = email_msg.decode("utf-8", "ignore")
|
||||
if (found := pattern.findall(email_msg)) and len(found) > 0:
|
||||
tracking_num = found[0]
|
||||
# DHL is special
|
||||
if " " in the_format:
|
||||
tracking_num = tracking_num.split(" ")[1]
|
||||
|
||||
_LOGGER.debug("Found tracking number in email body: %s", tracking_num)
|
||||
return tracking_num
|
||||
return None
|
||||
|
||||
|
||||
def save_image_data_to_disk(shipper_name: str, path: str, image_data: bytes) -> bool:
|
||||
"""Write image bytes to disk and verify."""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
directory = Path(path).parent
|
||||
if not directory.is_dir():
|
||||
_LOGGER.debug("%s - Creating directory: %s", shipper_name, directory)
|
||||
try:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error creating directory: %s", err)
|
||||
return False
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Writing %d bytes to file: %s",
|
||||
shipper_name,
|
||||
len(image_data),
|
||||
path,
|
||||
)
|
||||
with Path(path).open("wb") as the_file:
|
||||
the_file.write(image_data)
|
||||
|
||||
except OSError as err:
|
||||
_LOGGER.error(
|
||||
"Error saving %s delivery photo to %s: %s",
|
||||
shipper_name,
|
||||
path,
|
||||
err,
|
||||
)
|
||||
return False
|
||||
else:
|
||||
if Path(path).exists():
|
||||
return True
|
||||
|
||||
_LOGGER.error(
|
||||
"%s - ERROR: File write reported success but file doesn't exist: %s",
|
||||
shipper_name,
|
||||
path,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def generic_delivery_image_extraction(
|
||||
sdata: Any,
|
||||
image_path: str,
|
||||
image_name: str,
|
||||
shipper_name: str,
|
||||
image_type: str,
|
||||
cid_name: str | None = None,
|
||||
attachment_filename_pattern: str | None = None,
|
||||
) -> bool:
|
||||
"""Extract delivery photos from email."""
|
||||
_LOGGER.debug("Attempting to extract %s delivery photo", shipper_name)
|
||||
|
||||
msg = (
|
||||
email.message_from_bytes(sdata)
|
||||
if isinstance(sdata, (bytes, bytearray))
|
||||
else email.message_from_string(sdata)
|
||||
)
|
||||
|
||||
normalized_image_path = image_path.rstrip("/") + "/"
|
||||
shipper_path = f"{normalized_image_path}{shipper_name}/"
|
||||
full_path = shipper_path + image_name
|
||||
|
||||
# Pass 1: CID search
|
||||
if cid_name and (
|
||||
found := _extract_from_cid(msg, cid_name, shipper_name, full_path, image_type)
|
||||
):
|
||||
return found
|
||||
|
||||
# Pass 2: HTML/Base64 search
|
||||
if found := _extract_from_html(msg, cid_name, shipper_name, full_path, image_type):
|
||||
return found
|
||||
|
||||
# Pass 3: Attachment search
|
||||
return _extract_from_attachments(
|
||||
msg,
|
||||
attachment_filename_pattern,
|
||||
shipper_name,
|
||||
full_path,
|
||||
image_type,
|
||||
)
|
||||
|
||||
|
||||
def _extract_from_cid(
|
||||
msg: email.message.Message,
|
||||
cid_name: str,
|
||||
shipper_name: str,
|
||||
full_path: str,
|
||||
image_type: str,
|
||||
) -> bool:
|
||||
"""Pass 1: Look for CID embedded images."""
|
||||
content_type = f"image/{image_type}"
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == content_type:
|
||||
content_id = part.get("Content-ID")
|
||||
if content_id and content_id.strip("<>") == cid_name:
|
||||
return save_image_data_to_disk(
|
||||
shipper_name,
|
||||
full_path,
|
||||
part.get_payload(decode=True),
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _extract_from_html(
|
||||
msg: email.message.Message,
|
||||
cid_name: str | None,
|
||||
shipper_name: str,
|
||||
full_path: str,
|
||||
image_type: str,
|
||||
) -> bool:
|
||||
"""Pass 2: Look for HTML content with CID references or base64."""
|
||||
base64_pattern = rf"data:image/{image_type};base64,((?:[A-Za-z0-9+/]{{4}})*(?:[A-Za-z0-9+/]{{2}}==|[A-Za-z0-9+/]{{3}}=)?)"
|
||||
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() != "text/html":
|
||||
continue
|
||||
|
||||
payload = part.get_payload(decode=True)
|
||||
content = (
|
||||
payload.decode("utf-8", "ignore")
|
||||
if isinstance(payload, bytes)
|
||||
else str(payload)
|
||||
)
|
||||
|
||||
# Base64 check
|
||||
if matches := re.findall(base64_pattern, content):
|
||||
try:
|
||||
base64_data = matches[0].replace(" ", "").replace("=3D", "=")
|
||||
return save_image_data_to_disk(
|
||||
shipper_name,
|
||||
full_path,
|
||||
base64.b64decode(base64_data),
|
||||
)
|
||||
except (OSError, ValueError, TypeError) as err:
|
||||
_LOGGER.error(
|
||||
"Error saving %s delivery photo from base64: %s",
|
||||
shipper_name,
|
||||
err,
|
||||
)
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _extract_from_attachments(
|
||||
msg: email.message.Message,
|
||||
pattern: str | None,
|
||||
shipper_name: str,
|
||||
full_path: str,
|
||||
image_type: str,
|
||||
) -> bool:
|
||||
"""Pass 3: Look for attachments."""
|
||||
content_type = f"image/{image_type}"
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == content_type:
|
||||
filename = part.get_filename()
|
||||
if not filename:
|
||||
continue
|
||||
if pattern and pattern.lower() not in filename.lower():
|
||||
continue
|
||||
|
||||
try:
|
||||
return save_image_data_to_disk(
|
||||
shipper_name,
|
||||
full_path,
|
||||
part.get_payload(decode=True),
|
||||
)
|
||||
except (OSError, ValueError, TypeError) as err:
|
||||
_LOGGER.error(
|
||||
"Error saving %s delivery photo to %s: %s",
|
||||
shipper_name,
|
||||
full_path,
|
||||
err,
|
||||
)
|
||||
return False
|
||||
return False
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 171 B |
@@ -894,6 +894,10 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
unsub_digest,
|
||||
]
|
||||
|
||||
# Once HA has started (all entries loaded), flag object entries that were
|
||||
# orphaned by a deleted global entry as a fixable repair issue (#86).
|
||||
async_at_started(hass, _sync_missing_global_entry_issue)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -1058,6 +1062,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
|
||||
# HA-started so the store has finished loading.
|
||||
entry.async_on_unload(async_at_started(hass, _check_document_storage_issues))
|
||||
|
||||
# A global entry now exists — clear the orphan repair issue immediately
|
||||
# (e.g. right after the repair flow recreated it, when HA is already
|
||||
# started and the async_at_started check above won't fire again).
|
||||
_sync_missing_global_entry_issue(hass)
|
||||
|
||||
_LOGGER.debug("Global config entry set up: %s", entry.entry_id)
|
||||
else:
|
||||
# Maintenance object entry: create Store + coordinator
|
||||
@@ -1188,6 +1197,39 @@ async def _check_document_storage_issues(hass: HomeAssistant) -> None:
|
||||
ir.async_delete_issue(hass, DOMAIN, _DOC_STORAGE_ISSUE_ID)
|
||||
|
||||
|
||||
_MISSING_GLOBAL_ISSUE_ID = "missing_global_entry"
|
||||
|
||||
|
||||
@callback
|
||||
def _sync_missing_global_entry_issue(hass: HomeAssistant) -> None:
|
||||
"""Surface "object entries exist but the global entry is gone" as a repair.
|
||||
|
||||
The config flow always creates the global "Maintenance Supporter" entry
|
||||
before the first object, but a user can later delete just that entry from
|
||||
Settings → Devices & Services, leaving orphan object entries behind. That
|
||||
strips the summary sensors, the sidebar panel and the digests — and the
|
||||
dashboard KPI chips read "unknown" (#86, 2nd report). The fixable issue's
|
||||
flow recreates the global entry with default settings; it clears itself once
|
||||
a global entry is present again.
|
||||
"""
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
has_global = any(e.unique_id == GLOBAL_UNIQUE_ID for e in entries)
|
||||
has_objects = any(e.unique_id != GLOBAL_UNIQUE_ID for e in entries)
|
||||
if has_objects and not has_global:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
_MISSING_GLOBAL_ISSUE_ID,
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key=_MISSING_GLOBAL_ISSUE_ID,
|
||||
)
|
||||
else:
|
||||
ir.async_delete_issue(hass, DOMAIN, _MISSING_GLOBAL_ISSUE_ID)
|
||||
|
||||
|
||||
async def _async_global_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""React to global options changes (panel toggle / sidebar title)."""
|
||||
panel_enabled = entry.options.get(CONF_PANEL_ENABLED, DEFAULT_PANEL_ENABLED)
|
||||
@@ -1349,6 +1391,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: MaintenanceSupporterCon
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Clean up store file when a config entry is permanently deleted."""
|
||||
if entry.unique_id == GLOBAL_UNIQUE_ID:
|
||||
# Deleting the global entry while object entries remain is a broken
|
||||
# state (no summary sensors / panel / digests) — surface it right away
|
||||
# as a fixable repair issue so the user can restore it (#86).
|
||||
_sync_missing_global_entry_issue(hass)
|
||||
return
|
||||
store = MaintenanceStore(hass, entry.entry_id)
|
||||
await store.async_remove()
|
||||
|
||||