Updated apps

This commit is contained in:
2026-07-09 13:30:12 -04:00
parent 0f7585754d
commit 6cc7212cef
262 changed files with 16124 additions and 107 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+1 -1
View File
@@ -1 +1 @@
{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783526323.4156625}
{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783610735.5698383}
+288 -63
View File
@@ -467,14 +467,14 @@
- action: persistent_notification.create
data:
title: Dishwasher Filter Maintenance
message: The dishwasher has completed 40 cycles. Please clean the physical mesh
message: The dishwasher has completed 30 cycles. Please clean the physical mesh
filter and press Reset on your dashboard.
notification_id: dishwasher_filter_reminder
- action: notify.notify
metadata: {}
data:
title: Dishwasher Filter Maintenance
message: The dishwasher has completed 40 cycles. Please clean the physical mesh
message: The dishwasher has completed 30 cycles. Please clean the physical mesh
filter and press Reset on your dashboard.
mode: single
- id: '1781283661169'
@@ -701,45 +701,56 @@
- id: '1781445544080'
alias: 'Kitchen: Coffee Maker Cycle Status'
description: 'Triggers on the high-wattage grinder spike. Dynamically waits for
the cycle to finish: it requires a minimum of 4 minutes to pass AND the power
to drop back to the TV baseline.'
the cycle to finish: it requires a minimum of 1 minute to pass AND the power to
drop back to the TV baseline.'
triggers:
- trigger: state
- trigger: numeric_state
entity_id: sensor.coffee_maker_power
conditions:
- condition: template
value_template: '{{ states(''sensor.coffee_maker_power'') | float(0) > 1000 }}'
- condition: state
entity_id: input_boolean.coffee_maker_running
state: 'off'
above: 800
id: coffee_started
- trigger: numeric_state
entity_id: sensor.coffee_maker_power
below: 15
for:
hours: 0
minutes: 1
seconds: 0
id: coffee_finished
conditions: []
actions:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.coffee_maker_running
- action: notify.notify
data:
title: ☕ Coffee Time
message: Someone is making coffee.
- action: notify.persistent_notification
data:
title: ☕ Coffee Time
message: Someone is making coffee.
- delay:
hours: 0
minutes: 4
seconds: 0
- wait_template: '{{ states(''sensor.coffee_maker_power'') | float(0) < 350 }}'
timeout:
hours: 0
minutes: 5
seconds: 0
- action: input_boolean.turn_off
target:
entity_id: input_boolean.coffee_maker_running
- action: notify.notify
data:
title: ☕ Coffee Ready
message: Your coffee is ready to enjoy!
- choose:
- conditions:
- condition: trigger
id: coffee_started
- condition: state
entity_id: input_boolean.coffee_maker_running
state: 'off'
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.coffee_maker_running
- action: notify.notify
data:
title: ☕ Coffee Time
message: Someone is making coffee.
- action: notify.persistent_notification
data:
title: ☕ Coffee Time
message: Someone is making coffee.
- conditions:
- condition: trigger
id: coffee_finished
- condition: state
entity_id: input_boolean.coffee_maker_running
state: 'on'
sequence:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.coffee_maker_running
- action: notify.notify
data:
title: ☕ Coffee Ready
message: Your coffee is ready to enjoy!
mode: single
- id: '1781793117883'
alias: Basement Temperature Change
@@ -806,14 +817,16 @@
data:
title: "\U0001F6A8 Commute Delay Alert!"
message: "Traffic is heavier than usual this morning. \nGoogle Maps: {{ states('sensor.google_travel_time_work_work')
}} mins. Waze: {{ states('sensor.waze_travel_time') }} mins."
}} mins. Waze: {{ states('sensor.waze_to_work_travel_time') }} mins."
data:
image: /local/commute_snapshot.jpg
enabled: false
- action: notify.persistent_notification
data:
title: "\U0001F6A8 Commute Delay Alert!"
message: "Traffic is heavier than usual this morning. \nGoogle Maps: {{ states('sensor.google_travel_time_work_work')
}} mins. Waze: {{ states('sensor.waze_travel_time') }} mins.\n![Commute Map](/local/commute_snapshot.jpg)"
}} mins. Waze: {{ states('sensor.waze_to_work_travel_time') }} mins.\n![Commute
Map](/local/commute_snapshot.jpg)"
mode: single
- *id001
- *id001
@@ -863,7 +876,7 @@
data:
title: "\U0001F697 Morning Commute"
message: "Google: {{ states('sensor.google_travel_time_work_work') }} min\nWaze:
{{ states('sensor.waze_travel_time') }} min\n{% set g = states('sensor.google_travel_time_work_work')
{{ states('sensor.waze_to_work_travel_time') }} min\n{% set g = states('sensor.google_travel_time_work_work')
| float(0) %} {% if g > 50 %} \U0001F6A8 Major traffic delays. {% elif g >
40 %} ⚠️ Heavy traffic. {% else %} \U0001F7E1 Moderate traffic. {% endif %}"
mode: single
@@ -891,38 +904,96 @@
action_rotate_ccw_face_1: []
action_flipped90_face_1: []
- id: '1783162325964'
alias: Backyard Shed Camera Motion AI Description
description: Takes a snapshot when the Blink camera detects motion, analyzes it
with AI, and sends a mobile and persistent notification.
alias: Motion Shed - AI Description
description: Takes a snapshot when the Nest camera detects motion, a person, an
animal, or a doorbell press, analyzes it with AI, and sends a mobile and persistent
notification.
triggers:
- trigger: event
event_type: state_changed
event_data:
entity_id: event.front_door_front_door_motion
enabled: false
- trigger: state
entity_id: binary_sensor.backyard_shed_motion
to: 'on'
entity_id: event.living_room_living_room_camera_motion
enabled: false
- trigger: state
entity_id: event.front_door_front_door_motion
enabled: false
- trigger: event.received
target:
entity_id: event.entryway_entryway_camera_motion
options:
event_type:
- camera_motion
- camera_person
- camera_sound
enabled: false
- trigger: event.received
target:
device_id: 290f56e5a655e3af82cbf3cf9fd22406
options:
event_type:
- camera_motion
- camera_person
- camera_sound
- ring
enabled: false
- trigger: event.received
target:
device_id: 679d52e0d16cf48648d11dd024b961f8
options:
event_type:
- camera_motion
- camera_sound
- camera_person
enabled: true
conditions:
- condition: template
value_template: '{{ trigger.event.data.new_state.attributes.event_type in [''motion'',
''person'', ''animal'', ''chime''] }}'
enabled: false
actions:
- action: blink.trigger_camera
target:
entity_id: camera.backyard_shed
- delay: 00:00:02
data: {}
- delay:
hours: 0
minutes: 0
seconds: 5
milliseconds: 0
- action: homeassistant.update_entity
target:
entity_id: camera.backyard_shed
- variables:
snapshot_filename: blink_shed_{{ now().strftime('%Y%m%d_%H%M%S') }}.jpg
- variables:
notification_id: blink_shed_{{ now().strftime('%Y%m%d_%H%M%S_%f') }}
- action: camera.snapshot
target:
entity_id: camera.backyard_shed
data:
filename: /media/blink_shed.jpg
filename: /media/{{ snapshot_filename }}
- action: camera.snapshot
target:
entity_id: camera.backyard_shed
data:
filename: /config/www/blink_shed.jpg
- delay: 00:00:02
filename: /config/www/{{ snapshot_filename }}
- delay:
hours: 0
minutes: 0
seconds: 4
milliseconds: 0
- action: ai_task.generate_data
continue_on_error: true
data:
task_name: Backyard Shed Camera Analysis
task_name: Blink Shed Camera Analysis
entity_id: ai_task.google_ai_task
instructions: Describe what you see in this image in brief. Focus on any people,
objects, animals or activities. Text needs to be max 240 characters.
attachments:
- media_content_id: media-source://media_source/local/blink_shed.jpg
- media_content_id: media-source://media_source/local/{{ snapshot_filename }}
media_content_type: image/jpeg
response_variable: ai_profile
- action: notify.notify
@@ -932,16 +1003,15 @@
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}}'
- action: notify.persistent_notification
- action: persistent_notification.create
data:
title: Motion Detected - Backyard Shed
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}}
![Snapshot](/local/blink_shed.jpg)'
data:
notification_id: backyard_shed_motion
![Snapshot](/local/{{ snapshot_filename }})'
notification_id: '{{ notification_id }}'
mode: queued
max: 10
- id: '1783293479398'
@@ -1115,7 +1185,7 @@
![Snapshot](/local/blinkTree.jpg)'
mode: queued
max: 10
- id: '1783469453396'
- id: '1783540959739'
alias: Motion Front Door - AI Description
description: Takes a snapshot when the Nest camera detects motion, a person, an
animal, or a doorbell press, analyzes it with AI, and sends a mobile and persistent
@@ -1169,18 +1239,33 @@
target:
entity_id: camera.front
data: {}
- delay: 00:00:02
- delay:
hours: 0
minutes: 0
seconds: 5
milliseconds: 0
- action: homeassistant.update_entity
target:
entity_id: camera.front
- variables:
snapshot_filename: blink_Front_{{ now().strftime('%Y%m%d_%H%M%S') }}.jpg
- variables:
notification_id: blink_front_{{ now().strftime('%Y%m%d_%H%M%S_%f') }}
- action: camera.snapshot
target:
entity_id: camera.front
data:
filename: /media/blink_Front.jpg
filename: /media/{{ snapshot_filename }}
- action: camera.snapshot
target:
entity_id: camera.front
data:
filename: /config/www/blink_Front.jpg
- delay: 00:00:02
filename: /config/www/{{ snapshot_filename }}
- delay:
hours: 0
minutes: 0
seconds: 4
milliseconds: 0
- action: ai_task.generate_data
continue_on_error: true
data:
@@ -1189,7 +1274,7 @@
instructions: Describe what you see in this image in brief. Focus on any people,
objects, animals or activities. Text needs to be max 240 characters.
attachments:
- media_content_id: media-source://media_source/local/blink_Front.jpg
- media_content_id: media-source://media_source/local/{{ snapshot_filename }}
media_content_type: image/jpeg
response_variable: ai_profile
- action: notify.notify
@@ -1206,8 +1291,148 @@
else ''Motion detected, but the AI Task integration failed to return a response.''
}}
![Snapshot](/local/blink_Front.jpg)'
![Snapshot](/local/{{ snapshot_filename }})'
data:
notification_id: nest_front_motion
enabled: false
- action: persistent_notification.create
metadata: {}
data:
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}} ![Snapshot](/local/{{ snapshot_filename }})'
title: Motion Detected - Front Door
notification_id: '{{ notification_id }}'
mode: queued
max: 10
- id: '1783541174963'
alias: Daily Snapshot Purge
description: Triggers the snapshot purge script every night at midnight.
triggers:
- trigger: time
at: 00:00:00
conditions: []
actions:
- action: script.purge_old_blink_snapshot
data: {}
mode: single
- id: '1783615071224'
alias: 'TEST: AI Camera Motion Detection OLLAMA'
description: Takes a snapshot when the Nest camera detects motion, a person, an
animal, or a doorbell press, analyzes it with AI, and sends a mobile and persistent
notification.
triggers:
- trigger: event
event_type: state_changed
event_data:
entity_id: event.front_door_front_door_motion
enabled: false
- trigger: state
entity_id: event.living_room_living_room_camera_motion
enabled: false
- trigger: state
entity_id: event.front_door_front_door_motion
enabled: false
- trigger: event.received
target:
entity_id: event.entryway_entryway_camera_motion
options:
event_type:
- camera_motion
- camera_person
- camera_sound
enabled: false
- trigger: event.received
target:
device_id: 290f56e5a655e3af82cbf3cf9fd22406
options:
event_type:
- camera_motion
- camera_person
- camera_sound
- ring
- trigger: event.received
target:
device_id: 679d52e0d16cf48648d11dd024b961f8
options:
event_type:
- camera_motion
- camera_sound
- camera_person
enabled: false
conditions:
- condition: template
value_template: '{{ trigger.event.data.new_state.attributes.event_type in [''motion'',
''person'', ''animal'', ''chime''] }}'
enabled: false
actions:
- action: blink.trigger_camera
target:
entity_id: camera.front
data: {}
- delay:
hours: 0
minutes: 0
seconds: 10
milliseconds: 0
- action: homeassistant.update_entity
target:
entity_id: camera.front
- variables:
snapshot_filename: blink_Front_{{ now().strftime('%Y%m%d_%H%M%S') }}.jpg
- variables:
notification_id: blink_front_{{ now().strftime('%Y%m%d_%H%M%S_%f') }}
- action: camera.snapshot
target:
entity_id: camera.front
data:
filename: /media/{{ snapshot_filename }}
- action: camera.snapshot
target:
entity_id: camera.front
data:
filename: /config/www/{{ snapshot_filename }}
- delay:
hours: 0
minutes: 0
seconds: 4
milliseconds: 0
- action: ai_task.generate_data
continue_on_error: true
data:
task_name: Nest Front Camera Analysis
entity_id: ai_task.ollama_ai_task
instructions: Describe what you see in this image in brief. Focus on any people,
objects, animals or activities. Text needs to be max 240 characters.
attachments:
- media_content_id: media-source://media_source/local/{{ snapshot_filename }}
media_content_type: image/jpeg
response_variable: ai_profile
- action: notify.notify
continue_on_error: true
data:
title: Motion Detected - Front Door
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}}'
- action: notify.persistent_notification
data:
title: Motion Detected - Front Door
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}}
![Snapshot](/local/{{ snapshot_filename }})'
data:
notification_id: nest_front_motion
enabled: false
- action: persistent_notification.create
metadata: {}
data:
message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile
else ''Motion detected, but the AI Task integration failed to return a response.''
}} ![Snapshot](/local/{{ snapshot_filename }})'
title: Motion Detected - Front Door
notification_id: '{{ notification_id }}'
mode: queued
max: 10
@@ -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()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1 @@
# Placeholder to keep default images directory
Binary file not shown.

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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

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("&amp;", "&")) 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

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()

Some files were not shown because too many files have changed in this diff Show More