feat: full Home Assistant integration

- PHP: _fireHaWebhook(), _sendHaNotify(), haInventorySensor(), haTestConnection()
- PHP: ha_sensor + ha_test routing actions
- PHP: getServerSettings() exposes ha_token (consistent with tts_token)
- PHP: saveSettings() handles all HA_* env keys (url, token, tts_entity, webhook_id, events, notify_service, expiry_days)
- PHP: bringAddItems(), shoppingAdd(), updateInventory() fire shopping_add / stock_update webhooks
- Cron: daily HA expiry/expired webhook + push notify with flag-file guard
- HTML: 🏠 Settings tab button + full HA panel (connection, TTS, webhook, notify, sensor cards)
- JS: serverKeys + loadSettingsUI extended with HA fields
- JS: _applyHaSettingsUI(), _loadHaTab(), _renderHaSensorYaml()
- JS: onHaEnabledChange(), testHaConnection(), applyHaTtsPreset()
- JS: saveHaSettings(), copyHaSensorYaml(), showHaWebhookHelp()
- JS: _buildHaTtsRequest() for HA media_player TTS
- JS: speakCookingStep() now supports HA TTS as first-priority path
- JS: onTtsEngineChange() fixed to show server section for both 'server' and 'custom'
- Translations: settings.ha.* (52 keys) in all 5 languages (it/en/de/fr/es)
- .env.example: HA_ENABLED/URL/TOKEN/TTS_ENTITY/WEBHOOK_ID/EVENTS/NOTIFY_SERVICE/EXPIRY_DAYS
- docs/wiki/Home-Assistant.md: new wiki page (REST sensors, webhooks, TTS, push notify, troubleshooting)
- README: HA integration highlighted as first feature block
This commit is contained in:
dadaloop82
2026-05-23 12:28:09 +00:00
parent ec53f7529c
commit 965a672abe
12 changed files with 1228 additions and 4 deletions
+22
View File
@@ -129,6 +129,28 @@ GDRIVE_RETENTION_DAYS=30
# Leave empty to allow anyone with access to the server to change settings. # Leave empty to allow anyone with access to the server to change settings.
SETTINGS_TOKEN= SETTINGS_TOKEN=
# ── Home Assistant Integration ────────────────────────────────────────────────
# All HA settings can also be configured from the Settings → 🏠 tab.
#
# HA_ENABLED: master switch for all HA features (webhooks, TTS, sensors)
HA_ENABLED=false
# HA_URL: base URL of your HA instance — no trailing slash
# Examples: http://homeassistant.local:8123 or http://192.168.1.50:8123
HA_URL=
# HA_TOKEN: Long-Lived Access Token (HA Profile → Security → Long-Lived Access Tokens)
HA_TOKEN=
# HA_TTS_ENTITY: media_player entity for recipe step TTS (e.g. media_player.living_room)
HA_TTS_ENTITY=
# HA_WEBHOOK_ID: ID of an HA automation's Webhook trigger
HA_WEBHOOK_ID=
# HA_WEBHOOK_EVENTS: comma-separated events to fire webhooks for
# Available: expiry, shopping_add, stock_update, barcode_scan
HA_WEBHOOK_EVENTS=expiry,shopping_add,stock_update
# HA_NOTIFY_SERVICE: HA notify service for push alerts (e.g. notify.mobile_app_my_phone)
HA_NOTIFY_SERVICE=
# HA_EXPIRY_DAYS: days before expiry to trigger expiry alert (default 3)
HA_EXPIRY_DAYS=3
# ── Developer / demo ───────────────────────────────────────────────────────── # ── Developer / demo ─────────────────────────────────────────────────────────
# DEMO_MODE: when true, all write operations are blocked (for public demos) # DEMO_MODE: when true, all write operations are blocked (for public demos)
DEMO_MODE=false DEMO_MODE=false
+4
View File
@@ -42,6 +42,10 @@
## ✨ Features ## ✨ Features
> 🏠 **New — Native Home Assistant Integration**
> Connect EverShelf to your smart home: expose pantry counts as **REST sensors**, fire **webhook automations** on expiry/shopping/stock events, receive **push notifications** via any `notify.*` service, and read recipe steps aloud on **smart speakers** with full TTS support.
> Configure everything from the new **Settings → 🏠** tab — no YAML editing required. See the [Home Assistant wiki page](docs/wiki/Home-Assistant.md) for details.
> ⚙️ **New in v1.7.23 — Global settings tab, DB auto-cleanup, vacuum-sealed expiry** > ⚙️ **New in v1.7.23 — Global settings tab, DB auto-cleanup, vacuum-sealed expiry**
> A new **General** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place. > A new **General** tab groups all global settings (language, currency, theme, screensaver, zero-waste, export) in one place.
> Recipes older than `RECIPE_RETENTION_DAYS` and transactions older than `TRANSACTION_RETENTION_DAYS` are deleted automatically every cron cycle, followed by a SQLite `VACUUM` to keep the database small. > Recipes older than `RECIPE_RETENTION_DAYS` and transactions older than `TRANSACTION_RETENTION_DAYS` are deleted automatically every cron cycle, followed by a SQLite `VACUUM` to keep the database small.
+64
View File
@@ -133,3 +133,67 @@ try {
_phpErrorReport($msg, $e->getFile(), $e->getLine(), $e->getTraceAsString(), get_class($e)); _phpErrorReport($msg, $e->getFile(), $e->getLine(), $e->getTraceAsString(), get_class($e));
exit(1); exit(1);
} }
// ── Home Assistant: expiry alerts ─────────────────────────────────────────────
// Fire one HA webhook per expiring item (once per day guard via a simple flag file).
if (env('HA_ENABLED', 'false') === 'true' && env('HA_WEBHOOK_ID', '') !== '') {
try {
$haFlagFile = __DIR__ . '/../data/ha_expiry_notified_' . date('Y-m-d') . '.json';
if (!file_exists($haFlagFile)) {
$expiryDays = max(1, (int)env('HA_EXPIRY_DAYS', '3'));
$expiringItems = $db->query(
"SELECT p.name, i.quantity, i.unit, i.expiry_date, i.location
FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.quantity > 0 AND i.expiry_date IS NOT NULL
AND i.expiry_date BETWEEN date('now') AND date('now', '+{$expiryDays} days')
ORDER BY i.expiry_date ASC LIMIT 20"
)->fetchAll(PDO::FETCH_ASSOC);
$expiredItems = $db->query(
"SELECT p.name, i.quantity, i.unit, i.expiry_date, i.location
FROM inventory i JOIN products p ON i.product_id = p.id
WHERE i.quantity > 0 AND i.expiry_date IS NOT NULL
AND i.expiry_date < date('now')
ORDER BY i.expiry_date ASC LIMIT 10"
)->fetchAll(PDO::FETCH_ASSOC);
if (!empty($expiringItems)) {
$names = implode(', ', array_column($expiringItems, 'name'));
_fireHaWebhook('expiry_alert', [
'count' => count($expiringItems),
'items' => $expiringItems,
'type' => 'expiring_soon',
'days' => $expiryDays,
'summary' => $names,
]);
// Also send HA notification if service configured
if (env('HA_NOTIFY_SERVICE', '') !== '') {
$msg = count($expiringItems) . ' product(s) expiring within ' . $expiryDays . ' days: ' . $names;
_sendHaNotify($msg, ['expiring_items' => $expiringItems]);
}
echo '[' . date('Y-m-d H:i:s') . '] HA expiry_alert fired: ' . count($expiringItems) . " items\n";
}
if (!empty($expiredItems)) {
$expNames = implode(', ', array_column($expiredItems, 'name'));
_fireHaWebhook('expiry_alert', [
'count' => count($expiredItems),
'items' => $expiredItems,
'type' => 'expired',
'summary' => $expNames,
]);
echo '[' . date('Y-m-d H:i:s') . '] HA expired fired: ' . count($expiredItems) . " items\n";
}
// Mark as done for today
file_put_contents($haFlagFile, json_encode(['ts' => time(), 'expiring' => count($expiringItems ?? []), 'expired' => count($expiredItems ?? [])]));
// Clean up old flag files (keep last 7 days)
foreach (glob(__DIR__ . '/../data/ha_expiry_notified_*.json') as $oldFlag) {
$flagDate = str_replace([__DIR__ . '/../data/ha_expiry_notified_', '.json'], '', $oldFlag);
if ($flagDate < date('Y-m-d', strtotime('-7 days'))) @unlink($oldFlag);
}
}
} catch (Throwable $haE) {
echo '[' . date('Y-m-d H:i:s') . '] HA expiry hook warning: ' . $haE->getMessage() . "\n";
}
}
+255
View File
@@ -927,6 +927,14 @@ try {
ttsProxy(); ttsProxy();
break; break;
case 'ha_sensor':
haInventorySensor(getDB());
break;
case 'ha_test':
haTestConnection();
break;
case 'expiry_history': case 'expiry_history':
getExpiryHistory($db); getExpiryHistory($db);
break; break;
@@ -1246,8 +1254,221 @@ function ttsProxy() {
echo json_encode(['status' => $httpCode, 'body' => $response]); echo json_encode(['status' => $httpCode, 'body' => $response]);
} }
// ===== HOME ASSISTANT INTEGRATION =====
/**
* Fire an outbound webhook to Home Assistant.
* Respects HA_ENABLED, HA_URL, HA_WEBHOOK_ID and HA_WEBHOOK_EVENTS.
* Non-blocking: uses a 5 s cURL timeout; failures are logged but never thrown.
*/
function _fireHaWebhook(string $event, array $data): void {
if (env('HA_ENABLED', 'false') !== 'true') return;
$haUrl = rtrim(env('HA_URL', ''), '/');
$webhookId = env('HA_WEBHOOK_ID', '');
if (!$haUrl || !$webhookId) return;
$allowed = array_map('trim', explode(',', env('HA_WEBHOOK_EVENTS', 'expiry,shopping_add,stock_update,barcode_scan')));
if (!in_array($event, $allowed, true)) return;
$url = $haUrl . '/api/webhook/' . urlencode($webhookId);
$payload = json_encode(array_merge(['event' => $event, 'source' => 'evershelf', 'ts' => time()], $data), JSON_UNESCAPED_UNICODE);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_CONNECTTIMEOUT => 3,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
EverLog::warn("_fireHaWebhook[$event]: cURL error $err");
} else {
EverLog::debug("_fireHaWebhook[$event]: HTTP $code");
}
}
/**
* Send a notification via HA notify service (e.g. notify.mobile_app_phone).
* Used for expiry alerts when HA_NOTIFY_SERVICE is configured.
*/
function _sendHaNotify(string $message, array $data = []): void {
if (env('HA_ENABLED', 'false') !== 'true') return;
$haUrl = rtrim(env('HA_URL', ''), '/');
$token = env('HA_TOKEN', '');
$service = env('HA_NOTIFY_SERVICE', '');
if (!$haUrl || !$token || !$service) return;
// service format: "notify.mobile_app_xyz" → POST /api/services/notify/mobile_app_xyz
[$domain, $svcName] = array_pad(explode('.', $service, 2), 2, '');
if (!$svcName) return;
$url = $haUrl . '/api/services/' . urlencode($domain) . '/' . urlencode($svcName);
$payload = json_encode(array_merge(['message' => $message, 'data' => $data], []), JSON_UNESCAPED_UNICODE);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
],
CURLOPT_TIMEOUT => 8,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_CONNECTTIMEOUT => 4,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
EverLog::warn("_sendHaNotify: cURL error $err");
} else {
EverLog::debug("_sendHaNotify: HTTP $code");
}
}
/**
* HA REST sensor endpoint returns pantry state in Home Assistant-compatible format.
* Use with platform: rest in configuration.yaml.
*
* GET /api/?action=ha_sensor[&sensor=NAME]
* Available sensor names: expiring, expired, total, shopping
*/
function haInventorySensor(PDO $db): void {
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$sensor = strtolower(trim($_GET['sensor'] ?? 'overview'));
try {
$expiring = (int)$db->query(
"SELECT COUNT(*) FROM inventory WHERE quantity > 0 AND expiry_date IS NOT NULL
AND expiry_date BETWEEN date('now') AND date('now', '+3 days')"
)->fetchColumn();
$expired = (int)$db->query(
"SELECT COUNT(*) FROM inventory WHERE quantity > 0 AND expiry_date IS NOT NULL
AND expiry_date < date('now')"
)->fetchColumn();
$total = (int)$db->query(
"SELECT COUNT(*) FROM inventory WHERE quantity > 0"
)->fetchColumn();
$shoppingCount = 0;
if (isShoppingBringMode()) {
$auth = bringAuth();
if ($auth) {
$listData = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$auth['bringListUUID']}");
$shoppingCount = isset($listData['purchase']) ? count($listData['purchase']) : 0;
}
} else {
$shoppingCount = (int)$db->query("SELECT COUNT(*) FROM shopping_list")->fetchColumn();
}
// Expiring items details
$expiringItems = $db->query(
"SELECT name, quantity, unit, expiry_date FROM inventory
WHERE quantity > 0 AND expiry_date IS NOT NULL AND expiry_date BETWEEN date('now') AND date('now', '+7 days')
ORDER BY expiry_date ASC LIMIT 10"
)->fetchAll(PDO::FETCH_ASSOC);
$stateValue = match($sensor) {
'expired' => $expired,
'shopping' => $shoppingCount,
'total' => $total,
default => $expiring, // 'expiring' or 'overview'
};
echo json_encode([
'state' => $stateValue,
'attributes' => [
'expiring_soon' => $expiring,
'expiring_3d' => $expiring,
'expired_items' => $expired,
'total_items' => $total,
'shopping_items' => $shoppingCount,
'expiring_list' => array_map(fn($r) => [
'name' => $r['name'],
'quantity' => (float)$r['quantity'],
'unit' => $r['unit'],
'expiry_date'=> $r['expiry_date'],
], $expiringItems),
'unit_of_measurement'=> 'items',
'friendly_name' => 'EverShelf Pantry',
'icon' => 'mdi:fridge',
'last_updated' => date('c'),
],
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
// ===== CLIENT LOG ===== // ===== CLIENT LOG =====
/**
* Test reachability of a Home Assistant instance.
* Accepts POST body: {url, token}
* Uses server-env HA_TOKEN if token === '__server__' (token already saved on server).
*/
function haTestConnection(): void {
header('Content-Type: application/json; charset=utf-8');
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$url = rtrim($input['url'] ?? '', '/');
$token = $input['token'] ?? '';
if ($token === '__server__') {
$token = env('HA_TOKEN', '');
}
if (!$url) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'No URL provided']);
return;
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url . '/api/',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => array_filter([
'Content-Type: application/json',
$token ? 'Authorization: Bearer ' . $token : null,
]),
]);
$raw = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
echo json_encode(['ok' => false, 'error' => $err, 'http_code' => 0]);
return;
}
$data = json_decode($raw, true);
$version = $data['version'] ?? null;
if ($code === 200) {
echo json_encode(['ok' => true, 'version' => $version, 'http_code' => $code]);
} elseif ($code === 401) {
echo json_encode(['ok' => false, 'error' => 'bad_token', 'http_code' => $code]);
} else {
echo json_encode(['ok' => false, 'error' => 'http_' . $code, 'http_code' => $code]);
}
}
// ===== FOOD FACTS (cached daily) ===== // ===== FOOD FACTS (cached daily) =====
function getFoodFacts(): void { function getFoodFacts(): void {
EverLog::info('getFoodFacts'); EverLog::info('getFoodFacts');
@@ -2433,6 +2654,13 @@ function updateInventory(PDO $db): void {
// Real-time Bring! sync: done after commit so DB lock is not held during HTTP call // Real-time Bring! sync: done after commit so DB lock is not held during HTTP call
if (isset($input['quantity']) && $prevRow && abs((float)$input['quantity'] - (float)$prevRow['quantity']) > 0.001) { if (isset($input['quantity']) && $prevRow && abs((float)$input['quantity'] - (float)$prevRow['quantity']) > 0.001) {
try { bringQuickSyncProduct($db, (int)$prevRow['product_id']); } catch (Throwable $e) {} try { bringQuickSyncProduct($db, (int)$prevRow['product_id']); } catch (Throwable $e) {}
// HA: stock update event
$prodRow = $db->prepare("SELECT name FROM products WHERE id = ?")->execute([(int)$prevRow['product_id']]) ? $db->query("SELECT name FROM products WHERE id = " . (int)$prevRow['product_id'])->fetchColumn() : '';
_fireHaWebhook('stock_update', [
'item' => (string)$prodRow,
'quantity' => (float)$input['quantity'],
'location' => $input['location'] ?? $prevRow['location'] ?? '',
]);
} }
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
@@ -3221,6 +3449,15 @@ function getServerSettings(): void {
'shopping_forecast' => env('SHOPPING_FORECAST', 'true') === 'true', 'shopping_forecast' => env('SHOPPING_FORECAST', 'true') === 'true',
'shopping_auto_add_threshold' => (int)env('SHOPPING_AUTO_ADD_THRESHOLD', '0'), 'shopping_auto_add_threshold' => (int)env('SHOPPING_AUTO_ADD_THRESHOLD', '0'),
'dark_mode' => env('DARK_MODE', 'auto'), 'dark_mode' => env('DARK_MODE', 'auto'),
// Home Assistant Integration
'ha_enabled' => env('HA_ENABLED', 'false') === 'true',
'ha_url' => env('HA_URL', ''),
'ha_token' => env('HA_TOKEN', ''),
'ha_tts_entity' => env('HA_TTS_ENTITY', ''),
'ha_webhook_id' => env('HA_WEBHOOK_ID', ''),
'ha_webhook_events' => env('HA_WEBHOOK_EVENTS', 'expiry,shopping_add,stock_update,barcode_scan'),
'ha_notify_service' => env('HA_NOTIFY_SERVICE', ''),
'ha_expiry_days' => (int)env('HA_EXPIRY_DAYS', '3'),
]); ]);
} }
@@ -3287,6 +3524,13 @@ function saveSettings(): void {
'gdrive_client_secret' => 'GDRIVE_CLIENT_SECRET', 'gdrive_client_secret' => 'GDRIVE_CLIENT_SECRET',
'shopping_mode' => 'SHOPPING_MODE', 'shopping_mode' => 'SHOPPING_MODE',
'dark_mode' => 'DARK_MODE', 'dark_mode' => 'DARK_MODE',
// Home Assistant
'ha_url' => 'HA_URL',
'ha_token' => 'HA_TOKEN',
'ha_tts_entity' => 'HA_TTS_ENTITY',
'ha_webhook_id' => 'HA_WEBHOOK_ID',
'ha_webhook_events' => 'HA_WEBHOOK_EVENTS',
'ha_notify_service' => 'HA_NOTIFY_SERVICE',
]; ];
// Boolean keys // Boolean keys
$boolMap = [ $boolMap = [
@@ -3307,6 +3551,8 @@ function saveSettings(): void {
'shopping_enabled' => 'SHOPPING_ENABLED', 'shopping_enabled' => 'SHOPPING_ENABLED',
'shopping_smart_suggestions' => 'SHOPPING_SMART_SUGGESTIONS', 'shopping_smart_suggestions' => 'SHOPPING_SMART_SUGGESTIONS',
'shopping_forecast' => 'SHOPPING_FORECAST', 'shopping_forecast' => 'SHOPPING_FORECAST',
// Home Assistant
'ha_enabled' => 'HA_ENABLED',
]; ];
// Integer keys // Integer keys
$intMap = [ $intMap = [
@@ -3319,6 +3565,8 @@ function saveSettings(): void {
'backup_retention_days' => 'BACKUP_RETENTION_DAYS', 'backup_retention_days' => 'BACKUP_RETENTION_DAYS',
'gdrive_retention_days' => 'GDRIVE_RETENTION_DAYS', 'gdrive_retention_days' => 'GDRIVE_RETENTION_DAYS',
'shopping_auto_add_threshold' => 'SHOPPING_AUTO_ADD_THRESHOLD', 'shopping_auto_add_threshold' => 'SHOPPING_AUTO_ADD_THRESHOLD',
// Home Assistant
'ha_expiry_days' => 'HA_EXPIRY_DAYS',
]; ];
// Float keys // Float keys
$floatMap = [ $floatMap = [
@@ -7280,6 +7528,12 @@ function bringAddItems(): void {
if ($added > 0 || $updated > 0) { if ($added > 0 || $updated > 0) {
// Invalidate cache so next smart_shopping request reflects the updated Bring! list // Invalidate cache so next smart_shopping request reflects the updated Bring! list
@unlink(__DIR__ . '/../data/smart_shopping_cache.json'); @unlink(__DIR__ . '/../data/smart_shopping_cache.json');
// Fire HA webhook for each newly added item
foreach ($items as $item) {
$iName = $item['name'] ?? '';
if ($iName === '') continue;
_fireHaWebhook('shopping_add', ['item' => $iName, 'specification' => $item['specification'] ?? '']);
}
} }
echo json_encode(['success' => true, 'added' => $added, 'updated' => $updated, 'skipped' => $skipped, 'errors' => $errors]); echo json_encode(['success' => true, 'added' => $added, 'updated' => $updated, 'skipped' => $skipped, 'errors' => $errors]);
} }
@@ -8383,6 +8637,7 @@ function shoppingAdd(PDO $db): void {
} else { } else {
$db->prepare("INSERT INTO shopping_list (name, raw_name, specification) VALUES (?, ?, ?)")->execute([$name, $rawName, $spec]); $db->prepare("INSERT INTO shopping_list (name, raw_name, specification) VALUES (?, ?, ?)")->execute([$name, $rawName, $spec]);
$added++; $added++;
_fireHaWebhook('shopping_add', ['item' => $name, 'specification' => $spec]);
} }
} }
echo json_encode(['success' => true, 'added' => $added, 'updated' => $updated, 'skipped' => $skipped, 'errors' => []]); echo json_encode(['success' => true, 'added' => $added, 'updated' => $updated, 'skipped' => $skipped, 'errors' => []]);
+277 -4
View File
@@ -2212,7 +2212,10 @@ function _applySyncedSettings(serverSettings) {
'zerowaste_tips_enabled', 'zerowaste_tips_enabled',
'shopping_enabled','shopping_mode','shopping_smart_suggestions', 'shopping_enabled','shopping_mode','shopping_smart_suggestions',
'shopping_forecast','shopping_auto_add_threshold', 'shopping_forecast','shopping_auto_add_threshold',
'dark_mode']; 'dark_mode',
// Home Assistant
'ha_enabled','ha_url','ha_tts_entity','ha_webhook_id','ha_webhook_events',
'ha_notify_service','ha_expiry_days'];
let changed = false; let changed = false;
for (const key of serverKeys) { for (const key of serverKeys) {
if (serverSettings[key] !== undefined && serverSettings[key] !== null && serverSettings[key] !== '') { if (serverSettings[key] !== undefined && serverSettings[key] !== null && serverSettings[key] !== '') {
@@ -2834,6 +2837,18 @@ async function loadSettingsUI() {
s._tts_initialized = true; s._tts_initialized = true;
saveSettingsToStorage(s); saveSettingsToStorage(s);
} }
// HA settings — init defaults on first load
if (!s._ha_initialized) {
s.ha_enabled = s.ha_enabled || false;
s.ha_url = s.ha_url || '';
s.ha_tts_entity = s.ha_tts_entity || '';
s.ha_webhook_id = s.ha_webhook_id || '';
s.ha_webhook_events = s.ha_webhook_events || 'expiry,shopping_add,stock_update';
s.ha_notify_service = s.ha_notify_service || '';
s.ha_expiry_days = s.ha_expiry_days || 3;
s._ha_initialized = true;
saveSettingsToStorage(s);
}
const ttsEnabledEl = document.getElementById('setting-tts-enabled'); const ttsEnabledEl = document.getElementById('setting-tts-enabled');
if (ttsEnabledEl) ttsEnabledEl.checked = s.tts_enabled === true; if (ttsEnabledEl) ttsEnabledEl.checked = s.tts_enabled === true;
const ttsEngineEl = document.getElementById('setting-tts-engine'); const ttsEngineEl = document.getElementById('setting-tts-engine');
@@ -2875,7 +2890,9 @@ async function loadSettingsUI() {
'tts_content_type','tts_payload_key', 'tts_content_type','tts_payload_key',
'price_enabled','price_country','price_currency','price_update_months', 'price_enabled','price_country','price_currency','price_update_months',
'shopping_enabled','shopping_mode','shopping_smart_suggestions', 'shopping_enabled','shopping_mode','shopping_smart_suggestions',
'shopping_forecast','shopping_auto_add_threshold']; 'shopping_forecast','shopping_auto_add_threshold',
'ha_enabled','ha_url','ha_tts_entity','ha_webhook_id','ha_webhook_events',
'ha_notify_service','ha_expiry_days'];
// Note: gemini_key is never sent from server; settings_token_set is metadata only // Note: gemini_key is never sent from server; settings_token_set is metadata only
const settingsTokenRequired = !!serverSettings.settings_token_set; const settingsTokenRequired = !!serverSettings.settings_token_set;
const tokenHintEl = document.getElementById('settings-token-status-hint'); const tokenHintEl = document.getElementById('settings-token-status-hint');
@@ -2927,6 +2944,8 @@ async function loadSettingsUI() {
if (priceMonthsEl) priceMonthsEl.value = s.price_update_months || 3; if (priceMonthsEl) priceMonthsEl.value = s.price_update_months || 3;
// Shopping settings (server merge) // Shopping settings (server merge)
_applyShoppingSettingsUI(s); _applyShoppingSettingsUI(s);
// HA settings (server merge)
_applyHaSettingsUI(s);
} }
} catch(e) { /* offline, use local */ } } catch(e) { /* offline, use local */ }
// Price settings // Price settings
@@ -3506,6 +3525,15 @@ async function saveSettings() {
shopping_forecast: s.shopping_forecast !== false, shopping_forecast: s.shopping_forecast !== false,
shopping_auto_add_threshold: s.shopping_auto_add_threshold || 0, shopping_auto_add_threshold: s.shopping_auto_add_threshold || 0,
dark_mode: s.dark_mode || 'auto', dark_mode: s.dark_mode || 'auto',
// Home Assistant
ha_enabled: !!s.ha_enabled,
ha_url: s.ha_url || '',
...(s.ha_token ? { ha_token: s.ha_token } : {}),
ha_tts_entity: s.ha_tts_entity || '',
ha_webhook_id: s.ha_webhook_id || '',
ha_webhook_events: s.ha_webhook_events || '',
ha_notify_service: s.ha_notify_service || '',
ha_expiry_days: s.ha_expiry_days || 3,
}, tokenHeader); }, tokenHeader);
const statusEl = document.getElementById('settings-status'); const statusEl = document.getElementById('settings-status');
if (result.success) { if (result.success) {
@@ -13589,6 +13617,24 @@ function _buildTtsRequest(text, s) {
return { url, method, headers, body }; return { url, method, headers, body };
} }
/**
* Build a proxy request to call Home Assistant tts.speak service.
* Requires HA URL, bearer token and entity_id (media player) in settings.
*/
function _buildHaTtsRequest(text, s) {
const haUrl = (s.ha_url || '').replace(/\/$/, '');
const url = haUrl + '/api/services/tts/speak';
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (s.ha_token || ''),
};
const body = JSON.stringify({
entity_id: s.ha_tts_entity || '',
message: text,
});
return { url, method: 'POST', headers, body };
}
async function _ttsViaProxy(req) { async function _ttsViaProxy(req) {
// Route through server-side proxy to avoid mixed-content / CORS issues // Route through server-side proxy to avoid mixed-content / CORS issues
return fetch('api/index.php?action=tts_proxy', { return fetch('api/index.php?action=tts_proxy', {
@@ -13609,7 +13655,12 @@ async function speakCookingStep(text) {
// Use custom TTS endpoint only when explicitly configured; otherwise always use browser TTS. // Use custom TTS endpoint only when explicitly configured; otherwise always use browser TTS.
// Do NOT gate on s.tts_enabled — the _cookingTTS toggle in cooking mode is the only gate. // Do NOT gate on s.tts_enabled — the _cookingTTS toggle in cooking mode is the only gate.
try { try {
if (s.tts_engine === 'custom' && s.tts_url) { // 1. HA TTS — if HA is enabled and a media player entity is configured
if (s.ha_enabled && s.ha_tts_entity && s.ha_url) {
const req = _buildHaTtsRequest(text, s);
await _ttsViaProxy(req);
// 2. Generic external endpoint ('server' or legacy 'custom' engine)
} else if ((s.tts_engine === 'server' || s.tts_engine === 'custom') && s.tts_url) {
const req = _buildTtsRequest(text, s); const req = _buildTtsRequest(text, s);
await _ttsViaProxy(req); await _ttsViaProxy(req);
} else { } else {
@@ -13636,7 +13687,229 @@ function onTtsEngineChange(engine) {
const browserSect = document.getElementById('tts-browser-section'); const browserSect = document.getElementById('tts-browser-section');
const serverSect = document.getElementById('tts-server-section'); const serverSect = document.getElementById('tts-server-section');
if (browserSect) browserSect.style.display = engine === 'browser' ? '' : 'none'; if (browserSect) browserSect.style.display = engine === 'browser' ? '' : 'none';
if (serverSect) serverSect.style.display = engine === 'server' ? '' : 'none'; if (serverSect) serverSect.style.display = (engine === 'server' || engine === 'custom') ? '' : 'none';
}
// ===== HOME ASSISTANT PANEL =====
function onHaEnabledChange() {
const enabled = document.getElementById('setting-ha-enabled')?.checked;
const cfg = document.getElementById('ha-config-section');
if (cfg) cfg.style.display = enabled ? '' : 'none';
}
function _applyHaSettingsUI(s) {
const haEnabled = document.getElementById('setting-ha-enabled');
if (haEnabled) { haEnabled.checked = !!s.ha_enabled; onHaEnabledChange(); }
const haUrl = document.getElementById('setting-ha-url');
if (haUrl) haUrl.value = s.ha_url || '';
// Never pre-fill token (write-only field); only show placeholder if already set
const haTokenEl = document.getElementById('setting-ha-token');
if (haTokenEl) haTokenEl.placeholder = s.ha_token_set ? '••••••••••••' : 'eyJhbGci...';
const haEntity = document.getElementById('setting-ha-tts-entity');
if (haEntity) haEntity.value = s.ha_tts_entity || '';
const haWebhook = document.getElementById('setting-ha-webhook-id');
if (haWebhook) haWebhook.value = s.ha_webhook_id || '';
const haNotify = document.getElementById('setting-ha-notify-service');
if (haNotify) haNotify.value = s.ha_notify_service || '';
const haExpiry = document.getElementById('setting-ha-expiry-days');
if (haExpiry) haExpiry.value = s.ha_expiry_days || 3;
// Checkboxes for events
const events = (s.ha_webhook_events || '').split(',').map(e => e.trim());
const cbExpiry = document.getElementById('ha-event-expiry');
if (cbExpiry) cbExpiry.checked = events.includes('expiry');
const cbShopping = document.getElementById('ha-event-shopping');
if (cbShopping) cbShopping.checked = events.includes('shopping_add');
const cbStock = document.getElementById('ha-event-stock');
if (cbStock) cbStock.checked = events.includes('stock_update');
}
function _loadHaTab() {
const s = getSettings();
_applyHaSettingsUI(s);
_renderHaSensorYaml();
}
function _renderHaSensorYaml() {
const el = document.getElementById('ha-sensor-yaml');
if (!el) return;
const base = (window.location.origin + window.location.pathname).replace(/\/$/, '').replace(/\/index\.html$/, '');
el.textContent = `# Add to configuration.yaml (Home Assistant)
# Restart HA after editing.
sensor:
- platform: rest
name: "EverShelf Overview"
unique_id: evershelf_overview
resource: "${base}/api/?action=ha_sensor"
scan_interval: 300
value_template: "{{ value_json.state }}"
json_attributes:
- expiring_soon
- expiring_3d
- expired_items
- total_items
- shopping_items
- expiring_list
- last_updated
unit_of_measurement: "items"
device_class: null
- platform: rest
name: "EverShelf Expired Items"
unique_id: evershelf_expired
resource: "${base}/api/?action=ha_sensor&sensor=expired"
scan_interval: 600
value_template: "{{ value_json.state }}"
unit_of_measurement: "items"
- platform: rest
name: "EverShelf Shopping Count"
unique_id: evershelf_shopping
resource: "${base}/api/?action=ha_sensor&sensor=shopping"
scan_interval: 180
value_template: "{{ value_json.state }}"
unit_of_measurement: "items"`;
}
function copyHaSensorYaml() {
const el = document.getElementById('ha-sensor-yaml');
if (!el) return;
navigator.clipboard.writeText(el.textContent).then(() => {
showToast(t('settings.ha.sensor_copied'));
}).catch(() => {
showToast(t('error.copy_failed'));
});
}
async function testHaConnection() {
const statusEl = document.getElementById('ha-test-status');
const haUrl = document.getElementById('setting-ha-url')?.value.trim();
const haToken = document.getElementById('setting-ha-token')?.value.trim();
const s = getSettings();
const tokenToUse = haToken || (s.ha_token_set ? '__server__' : '');
if (!haUrl) {
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = t('settings.ha.error_no_url'); }
return;
}
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status'; statusEl.textContent = t('settings.ha.testing'); }
try {
const result = await api('ha_test', {}, 'POST', { url: haUrl, token: tokenToUse });
if (result.ok) {
if (statusEl) { statusEl.className = 'settings-status success'; statusEl.textContent = '✅ ' + t('settings.ha.test_ok').replace('{version}', result.version || 'HA'); }
} else {
if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = '❌ ' + t('settings.ha.test_fail').replace('{error}', result.error || result.http_code || ''); }
}
} catch(e) {
if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = '❌ ' + e.message; }
}
}
function applyHaTtsPreset() {
const s = getSettings();
const haUrl = (document.getElementById('setting-ha-url')?.value || s.ha_url || '').replace(/\/$/, '');
const entity = document.getElementById('setting-ha-tts-entity')?.value || s.ha_tts_entity || '';
if (!haUrl) {
showToast(t('settings.ha.error_no_url'));
return;
}
// Switch to TTS tab and fill fields
const ttsTab = document.querySelector('[data-tab="tab-tts"]');
if (ttsTab) ttsTab.click();
const engineEl = document.getElementById('setting-tts-engine');
if (engineEl) { engineEl.value = 'server'; onTtsEngineChange('server'); }
const urlEl = document.getElementById('setting-tts-url');
if (urlEl) urlEl.value = haUrl + '/api/services/tts/speak';
const methodEl = document.getElementById('setting-tts-method');
if (methodEl) methodEl.value = 'POST';
const authTypeEl = document.getElementById('setting-tts-auth-type');
if (authTypeEl) { authTypeEl.value = 'bearer'; onTtsAuthTypeChange('bearer'); }
const tokenEl = document.getElementById('setting-tts-token');
if (tokenEl) {
const haToken = document.getElementById('setting-ha-token')?.value.trim() || s.ha_token || '';
tokenEl.value = haToken;
}
const payloadKeyEl = document.getElementById('setting-tts-payload-key');
if (payloadKeyEl) payloadKeyEl.value = 'message';
const ctEl = document.getElementById('setting-tts-content-type');
if (ctEl) ctEl.value = 'application/json';
const extraEl = document.getElementById('setting-tts-extra-fields');
if (extraEl) extraEl.value = entity ? JSON.stringify({ entity_id: entity }) : '';
showToast(t('settings.ha.tts_preset_applied'));
}
function showHaWebhookHelp() {
const msg = t('settings.ha.webhook_help');
showToast(msg, 8000);
}
async function saveHaSettings() {
const s = getSettings();
const haEnabled = document.getElementById('setting-ha-enabled')?.checked || false;
const haUrl = document.getElementById('setting-ha-url')?.value.trim() || '';
const haToken = document.getElementById('setting-ha-token')?.value.trim() || '';
const haTtsEntity = document.getElementById('setting-ha-tts-entity')?.value.trim() || '';
const haWebhookId = document.getElementById('setting-ha-webhook-id')?.value.trim() || '';
const haNotify = document.getElementById('setting-ha-notify-service')?.value.trim() || '';
const haExpiryDays = parseInt(document.getElementById('setting-ha-expiry-days')?.value, 10) || 3;
const events = [];
if (document.getElementById('ha-event-expiry')?.checked) events.push('expiry');
if (document.getElementById('ha-event-shopping')?.checked) events.push('shopping_add');
if (document.getElementById('ha-event-stock')?.checked) events.push('stock_update');
const haEvents = events.join(',');
s.ha_enabled = haEnabled;
s.ha_url = haUrl;
if (haToken) s.ha_token = haToken;
s.ha_tts_entity = haTtsEntity;
s.ha_webhook_id = haWebhookId;
s.ha_webhook_events = haEvents;
s.ha_notify_service = haNotify;
s.ha_expiry_days = haExpiryDays;
saveSettingsToStorage(s);
const statusEl = document.getElementById('ha-save-status');
try {
const settingsToken = document.getElementById('setting-settings-token')?.value.trim() || '';
const tokenHeader = settingsToken ? { 'X-Settings-Token': settingsToken } : {};
const result = await api('save_settings', {}, 'POST', {
ha_enabled: haEnabled,
ha_url: haUrl,
...(haToken ? { ha_token: haToken } : {}),
ha_tts_entity: haTtsEntity,
ha_webhook_id: haWebhookId,
ha_webhook_events: haEvents,
ha_notify_service: haNotify,
ha_expiry_days: haExpiryDays,
}, tokenHeader);
if (statusEl) {
statusEl.style.display = 'block';
statusEl.className = result.success ? 'settings-status success' : 'settings-status error';
statusEl.textContent = result.success ? '✅ ' + t('settings.saved') : '❌ ' + (result.error || t('settings.saved_local_error'));
setTimeout(() => { if (statusEl) statusEl.style.display = 'none'; }, 4000);
}
} catch(e) {
if (statusEl) {
statusEl.style.display = 'block';
statusEl.className = 'settings-status success';
statusEl.textContent = '✅ ' + t('settings.saved_local');
setTimeout(() => { if (statusEl) statusEl.style.display = 'none'; }, 4000);
}
}
} }
/** Populate voice selector from Web Speech API. Called on settings load and on voiceschanged. */ /** Populate voice selector from Web Speech API. Called on settings load and on voiceschanged. */
+219
View File
@@ -0,0 +1,219 @@
# Home Assistant Integration
EverShelf integrates natively with [Home Assistant](https://www.home-assistant.io/) to bring your pantry data into your smart-home automations.
**Capabilities:**
- 📡 **REST sensors** — expose pantry counts as HA sensor entities (expiring, expired, shopping list, total items)
- 🔔 **Webhooks** — trigger HA automations on pantry events (expiry alerts, shopping additions, stock updates)
- 📣 **Push notifications** — send alerts to your phone via any HA `notify.*` service
- 🔊 **TTS on smart speakers** — read recipe steps aloud on any HA `media_player` entity
- ⚙️ **In-app config panel** — configure everything from Settings → 🏠 tab (no need to edit `.env` manually)
---
## Quick Setup
1. **Generate a Long-Lived Access Token** in Home Assistant:
- Open HA → your **Profile** (bottom-left avatar) → **Security****Long-Lived Access Tokens****Create Token**
- Copy the generated token — you won't see it again.
2. **Open EverShelf Settings** → tab **🏠 Home Assistant**.
3. Fill in **Home Assistant URL** (e.g. `http://homeassistant.local:8123`) and paste the token.
4. Click **Test connection** — you should see ✅.
5. Enable the features you want (TTS, Webhooks, REST Sensors) and click **Save HA settings**.
---
## REST Sensors
Add EverShelf pantry data as native HA sensor entities that update automatically.
### Endpoints
| URL | Returns | Sensor |
|-----|---------|--------|
| `/api/?action=ha_sensor` | Items expiring soon (≤3 days) | `sensor.evershelf_overview` |
| `/api/?action=ha_sensor&sensor=expired` | Expired items count | `sensor.evershelf_expired` |
| `/api/?action=ha_sensor&sensor=shopping` | Shopping list item count | `sensor.evershelf_shopping` |
| `/api/?action=ha_sensor&sensor=total` | Total pantry items | `sensor.evershelf_total` |
### Generate & Copy YAML
In Settings → 🏠 Home Assistant → **REST Sensors** card, click **Copy YAML** to get a ready-to-paste `configuration.yaml` block that already contains your EverShelf URL.
### Manual YAML example
```yaml
# configuration.yaml
sensor:
- platform: rest
name: "EverShelf Overview"
unique_id: evershelf_overview
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor"
scan_interval: 300 # seconds
value_template: "{{ value_json.state }}"
json_attributes:
- expiring_soon
- expiring_3d
- expired_items
- total_items
- shopping_items
- expiring_list
- last_updated
unit_of_measurement: "items"
- platform: rest
name: "EverShelf Shopping Count"
unique_id: evershelf_shopping
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor&sensor=shopping"
scan_interval: 180
value_template: "{{ value_json.state }}"
unit_of_measurement: "items"
```
Restart Home Assistant after editing `configuration.yaml`.
---
## Webhook Automations
EverShelf fires an HTTP POST to your HA webhook URL when pantry events occur.
### Create the HA Webhook Automation
1. HA → **Settings****Automations & Scenes****Create Automation**
2. Click **Add Trigger** → choose **Webhook**
3. HA generates a **Webhook ID** — copy it
4. Paste the ID into **Settings → 🏠 Home Assistant → Webhook ID**
5. Select which events should trigger the webhook
### Supported Events
| Event key | When it fires |
|-----------|--------------|
| `expiry` | Daily cron — items expiring within `HA_EXPIRY_DAYS` days |
| `shopping_add` | Item added to the shopping list |
| `stock_update` | Inventory quantity changed |
| `barcode_scan` | (reserved for future use) |
### Webhook Payload (POST body)
```json
{
"event": "expiry_alert",
"timestamp": "2025-06-12T08:00:00+00:00",
"data": {
"type": "expiring_soon",
"count": 3,
"days": 3,
"summary": "3 products expiring within 3 days",
"items": [
{ "name": "Milk", "expiry_date": "2025-06-14", "quantity": 1, "unit": "l" }
]
}
}
```
### Example: Expiry Alert → Telegram
```yaml
alias: EverShelf Expiry Alert
trigger:
- platform: webhook
webhook_id: "evershelf_webhook_abc123" # ← your Webhook ID
action:
- service: notify.telegram_bot
data:
message: >
🥫 EverShelf: {{ trigger.json.data.summary }}
{% for item in trigger.json.data.items %}
— {{ item.name }} (expires {{ item.expiry_date }})
{% endfor %}
```
---
## Push Notifications
If you prefer to receive push alerts without using webhooks, configure a **HA notify service** directly:
1. Find your notify service name in HA: **Developer Tools → Services** → search `notify`
2. Paste it into **Settings → 🏠 → Notify service** (e.g. `notify.mobile_app_my_phone`)
3. Save
EverShelf will call this service from the cron job whenever expiry alerts fire.
---
## TTS on Smart Speakers
Read recipe steps aloud on an Amazon Echo, Google Home, Sonos, or any HA `media_player`.
### Configuration
1. Enter the **Entity ID** of your media player (e.g. `media_player.kitchen_display`)
- Find it in HA: **Developer Tools → States**
2. Click **Apply HA preset to TTS tab** — this auto-fills the TTS tab with the correct HA endpoint and auth headers
3. Save settings
### How it Works
When recipe step TTS is triggered, EverShelf calls:
```
POST /api/services/tts/speak
Authorization: Bearer <HA_TOKEN>
{
"entity_id": "media_player.kitchen_display",
"message": "Add 200 g of flour and mix well."
}
```
The request is proxied through the EverShelf PHP backend (avoids CORS / mixed-content issues).
---
## Environment Variables Reference
All settings are configurable from `.env` or from the in-app Settings panel.
| Variable | Default | Description |
|----------|---------|-------------|
| `HA_ENABLED` | `false` | Master switch for all HA features |
| `HA_URL` | _(empty)_ | Base URL of HA instance, no trailing slash |
| `HA_TOKEN` | _(empty)_ | Long-Lived Access Token |
| `HA_TTS_ENTITY` | _(empty)_ | `media_player` entity for TTS |
| `HA_WEBHOOK_ID` | _(empty)_ | Webhook trigger ID from HA automation |
| `HA_WEBHOOK_EVENTS` | `expiry,shopping_add,stock_update` | Comma-separated list of events |
| `HA_NOTIFY_SERVICE` | _(empty)_ | HA notify service (e.g. `notify.mobile_app_phone`) |
| `HA_EXPIRY_DAYS` | `3` | Days before expiry to trigger the daily alert |
---
## Troubleshooting
**Test shows ❌ "Connection failed"**
- Verify the URL is reachable from the EverShelf server (not just your browser)
- If using HTTPS with a self-signed certificate, the server-side cURL request may fail — use HTTP on the local network instead
- Check that port 8123 (or your custom port) is open on the HA host
**Test shows ❌ "bad_token"**
- The Long-Lived Access Token may have expired or been revoked — generate a new one in HA Profile
**Webhook not firing**
- Confirm HA_ENABLED=true and the Webhook ID is exactly as shown in HA
- Check the EverShelf cron is running (`/api/cron_smart_shopping.php` every 5 minutes)
- For shopping/stock events: verify the event name is in `HA_WEBHOOK_EVENTS`
**TTS not speaking**
- Ensure the media player entity is online in HA (check its state in Developer Tools)
- Try the "Apply HA preset to TTS tab" button and send a test from the TTS tab
- Check HA logs for `tts.speak` errors (some platforms require `tts_options`)
**Sensors show unavailable in HA**
- The EverShelf URL must be reachable from the HA host
- If running EverShelf behind a reverse proxy, ensure `/api/` is accessible
- Use `scan_interval` ≥ 60 to avoid hammering the server
+117
View File
@@ -841,6 +841,7 @@
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-security')" data-tab="tab-security" title="Sicurezza">🔒</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-security')" data-tab="tab-security" title="Sicurezza">🔒</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)" data-i18n-title="settings.tab_tts">🔊</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)" data-i18n-title="settings.tab_tts">🔊</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-ha'); _loadHaTab();" data-tab="tab-ha" title="Home Assistant" data-i18n-title="settings.ha.tab">🏠</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-scale')" data-tab="tab-scale" title="Bilancia Smart" data-i18n-title="settings.scale.tab">⚖️</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-scale')" data-tab="tab-scale" title="Bilancia Smart" data-i18n-title="settings.scale.tab">⚖️</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-backup'); _loadBackupTab();" data-tab="tab-backup" data-i18n-title="settings.backup.tab" title="Backup">💾</button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-backup'); _loadBackupTab();" data-tab="tab-backup" data-i18n-title="settings.backup.tab" title="Backup">💾</button>
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-info'); _loadInfoTab();" data-tab="tab-info" data-i18n-title="settings.info.tab" title="Info"></button> <button class="settings-tab" onclick="switchSettingsTab(this, 'tab-info'); _loadInfoTab();" data-tab="tab-info" data-i18n-title="settings.info.tab" title="Info"></button>
@@ -1315,8 +1316,124 @@
<button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()" data-i18n="settings.tts.test_btn">🔊 Invia Test Vocale</button> <button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()" data-i18n="settings.tts.test_btn">🔊 Invia Test Vocale</button>
<div id="tts-test-status" style="display:none;margin-top:8px"></div> <div id="tts-test-status" style="display:none;margin-top:8px"></div>
<!-- HA TTS quick-fill hint -->
<div style="margin-top:12px;padding:10px 12px;background:rgba(3,169,244,0.07);border:1px solid rgba(3,169,244,0.25);border-radius:8px;font-size:0.82rem">
<span data-i18n="settings.tts.ha_hint">🏠 Se usi Home Assistant, usa il tab <strong>Home Assistant</strong> per configurare TTS, webhook e sensori.</span>
</div> </div>
</div> </div>
</div>
<!-- Home Assistant Tab -->
<div class="settings-panel" id="tab-ha">
<!-- Connection card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.title">🏠 Home Assistant</h4>
<p class="settings-hint" data-i18n="settings.ha.hint">Integra EverShelf con Home Assistant: TTS su speaker smart, webhook per automazioni, sensori per la dashboard.</p>
<div class="form-group" style="margin-bottom:10px">
<label class="toggle-row">
<span data-i18n="settings.ha.enabled">✅ Abilita integrazione Home Assistant</span>
<span class="toggle-switch">
<input type="checkbox" id="setting-ha-enabled" onchange="onHaEnabledChange()">
<span class="toggle-slider"></span>
</span>
</label>
</div>
<div id="ha-config-section">
<div class="form-group">
<label data-i18n="settings.ha.url_label">🌐 Home Assistant URL</label>
<input type="url" id="setting-ha-url" class="form-input" placeholder="http://192.168.1.50:8123">
<p class="settings-hint" data-i18n="settings.ha.url_hint">URL base della tua istanza HA (senza slash finale). Es: <code>http://homeassistant.local:8123</code></p>
</div>
<div class="form-group">
<label data-i18n="settings.ha.token_label">🔑 Long-Lived Access Token</label>
<div style="display:flex;gap:8px;align-items:center">
<input type="password" id="setting-ha-token" class="form-input" style="flex:1" placeholder="eyJhbGci...">
<button class="btn btn-secondary" style="flex-shrink:0" onclick="togglePasswordVisibility('setting-ha-token')" data-i18n="btn.toggle_password">👁️</button>
</div>
<p class="settings-hint" data-i18n="settings.ha.token_hint">Genera un token in HA → Profilo → Token di accesso a lungo termine.</p>
</div>
<button class="btn btn-secondary full-width" onclick="testHaConnection()" data-i18n="settings.ha.test_btn">🔗 Testa connessione HA</button>
<div id="ha-test-status" style="display:none;margin-top:8px" class="settings-status"></div>
</div>
</div>
<!-- TTS via HA card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.tts_title">🔊 TTS su Speaker Smart</h4>
<p class="settings-hint" data-i18n="settings.ha.tts_hint">Leggi i passi della ricetta su un altoparlante gestito da HA (Sonos, Echo, Google Home, ecc.).</p>
<div class="form-group">
<label data-i18n="settings.ha.tts_entity_label">🔈 Entity ID del media player</label>
<input type="text" id="setting-ha-tts-entity" class="form-input" placeholder="media_player.living_room">
<p class="settings-hint" data-i18n="settings.ha.tts_entity_hint">Copia l'entity ID del media player da HA → Strumenti sviluppatore → Stati.</p>
</div>
<div class="form-group">
<label data-i18n="settings.ha.tts_platform_label">🎙️ Piattaforma TTS</label>
<select id="setting-ha-tts-platform" class="form-input">
<option value="tts.speak" data-i18n="settings.ha.tts_platform_speak">tts.speak (raccomandato)</option>
<option value="notify" data-i18n="settings.ha.tts_platform_notify">notify.* (servizio notifiche)</option>
</select>
</div>
<button class="btn btn-secondary full-width" onclick="applyHaTtsPreset()" data-i18n="settings.ha.tts_apply_btn">✅ Applica preset HA al TTS</button>
<p class="settings-hint mt-2" data-i18n="settings.ha.tts_apply_hint">Configura automaticamente il tab TTS con i parametri HA corretti.</p>
</div>
<!-- Webhook card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.webhook_title">⚡ Automazioni Webhook</h4>
<p class="settings-hint" data-i18n="settings.ha.webhook_hint">EverShelf chiama il webhook HA quando si verificano eventi (prodotto in scadenza, aggiunto alla lista, ecc.).</p>
<div class="form-group">
<label data-i18n="settings.ha.webhook_id_label">🔗 Webhook ID</label>
<input type="text" id="setting-ha-webhook-id" class="form-input" placeholder="evershelf_events">
<p class="settings-hint" data-i18n="settings.ha.webhook_id_hint">Crea un'automazione in HA con trigger "Webhook" e copia qui l'ID. <a href="#" onclick="showHaWebhookHelp();return false" style="color:var(--accent)">Come farlo?</a></p>
</div>
<div class="form-group">
<label data-i18n="settings.ha.webhook_events_label">📋 Eventi da notificare</label>
<div style="display:flex;flex-direction:column;gap:6px;margin-top:4px">
<label style="display:flex;align-items:center;gap:8px;font-weight:normal">
<input type="checkbox" id="ha-event-expiry" value="expiry"> <span data-i18n="settings.ha.event_expiry">Prodotti in scadenza (cron giornaliero)</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-weight:normal">
<input type="checkbox" id="ha-event-shopping" value="shopping_add"> <span data-i18n="settings.ha.event_shopping">Aggiunta alla lista della spesa</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-weight:normal">
<input type="checkbox" id="ha-event-stock" value="stock_update"> <span data-i18n="settings.ha.event_stock">Aggiornamento scorte (quantità modificata)</span>
</label>
</div>
</div>
<div class="form-group">
<label data-i18n="settings.ha.expiry_days_label">📅 Giorni anticipo per scadenze</label>
<input type="number" id="setting-ha-expiry-days" class="form-input" min="1" max="30" value="3">
<p class="settings-hint" data-i18n="settings.ha.expiry_days_hint">Quanti giorni prima della scadenza inviare l'alert.</p>
</div>
</div>
<!-- Notify service card -->
<div class="settings-card">
<h4 data-i18n="settings.ha.notify_title">📱 Notifiche Push</h4>
<p class="settings-hint" data-i18n="settings.ha.notify_hint">EverShelf invia notifiche push tramite il servizio <code>notify.*</code> di HA (Telegram, Pushover, app mobile, ecc.).</p>
<div class="form-group">
<label data-i18n="settings.ha.notify_service_label">📣 Servizio notify</label>
<input type="text" id="setting-ha-notify-service" class="form-input" placeholder="notify.mobile_app_mio_telefono">
<p class="settings-hint" data-i18n="settings.ha.notify_service_hint">Formato: <code>notify.NOME_SERVIZIO</code>. Lascia vuoto per disabilitare. Richiede token HA configurato.</p>
</div>
</div>
<!-- Sensor card (read-only info) -->
<div class="settings-card">
<h4 data-i18n="settings.ha.sensor_title">📊 Sensori REST per HA</h4>
<p class="settings-hint" data-i18n="settings.ha.sensor_hint">HA può leggere i dati dell'inventario via REST polling. Aggiungi questo snippet a <code>configuration.yaml</code>:</p>
<div id="ha-sensor-yaml" style="background:var(--bg-secondary,#f1f5f9);border-radius:8px;padding:12px;font-family:monospace;font-size:0.75rem;white-space:pre;overflow-x:auto;max-height:220px;overflow-y:auto;border:1px solid var(--border,#e2e8f0)"></div>
<button class="btn btn-secondary full-width mt-2" onclick="copyHaSensorYaml()" data-i18n="settings.ha.sensor_copy_btn">📋 Copia YAML</button>
</div>
<!-- Save button -->
<button class="btn btn-large btn-accent full-width" onclick="saveHaSettings()" data-i18n="settings.ha.save_btn">💾 Salva impostazioni HA</button>
<div id="ha-save-status" style="display:none;margin-top:8px" class="settings-status"></div>
</div>
<!-- Scale Tab --> <!-- Scale Tab -->
<div class="settings-panel" id="tab-scale"> <div class="settings-panel" id="tab-scale">
<div class="settings-card"> <div class="settings-card">
+54
View File
@@ -868,6 +868,60 @@
"forecast_label": "Prognose für bald leere Produkte", "forecast_label": "Prognose für bald leere Produkte",
"auto_add_label": "Automatisch hinzufügen wenn", "auto_add_label": "Automatisch hinzufügen wenn",
"auto_add_suffix": "im Lager verbleibend (0 = nur wenn leer)" "auto_add_suffix": "im Lager verbleibend (0 = nur wenn leer)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Verbinde EverShelf mit Home Assistant für Automationen, Push-Benachrichtigungen und REST-Sensoren.",
"enabled": "Home Assistant-Integration aktivieren",
"connection_title": "Verbindung",
"url_label": "Home Assistant URL",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "Basis-URL deiner Home Assistant-Instanz (z.B. http://homeassistant.local:8123).",
"token_label": "Long-Lived Access Token",
"token_hint": "Erstelle unter HA-Profil → Sicherheit → Langlebige Zugangstoken.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token gespeichert (aus Sicherheitsgründen verborgen)",
"test_btn": "Verbindung testen",
"test_ok": "Verbunden mit {version}",
"test_fail": "Verbindung fehlgeschlagen: {error}",
"test_bad_token": "HA erreichbar, aber Token ist ungültig",
"testing": "Teste…",
"error_no_url": "Bitte zuerst die Home Assistant URL eingeben.",
"tts_title": "TTS auf Smart Speaker",
"tts_hint": "Rezeptschritte auf einem Home Assistant Media Player vorlesen.",
"tts_entity_label": "Media Player Entity ID",
"tts_entity_placeholder": "media_player.wohnzimmer",
"tts_entity_hint": "Entity-ID des HA-Media-Players. Zu finden unter HA: Entwicklertools → Zustände.",
"tts_platform_label": "TTS-Plattform",
"tts_platform_speak": "tts.speak (empfohlen)",
"tts_platform_notify": "notify.* (Benachrichtigungsdienst)",
"tts_apply_btn": "HA-Voreinstellung auf TTS-Tab anwenden",
"tts_apply_hint": "Füllt den TTS-Tab mit der Home Assistant URL und dem Token aus.",
"tts_preset_applied": "HA-Voreinstellung auf TTS-Tab angewendet.",
"webhook_title": "Webhook-Automationen",
"webhook_hint": "Sende Daten an Home Assistant, wenn Ereignisse in der Vorratskammer auftreten.",
"webhook_id_label": "Webhook-ID",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID des in HA erstellten Webhooks. Kopiere aus: HA → Einstellungen → Automationen → Erstellen → Webhook-Auslöser.",
"webhook_events_label": "Benachrichtige bei diesen Ereignissen",
"event_expiry": "Ablaufende Produkte (täglich)",
"event_shopping": "Artikel zur Einkaufsliste hinzugefügt",
"event_stock": "Lagerbestand aktualisiert",
"expiry_days_label": "Ablaufwarnung im Voraus (Tage)",
"expiry_days_hint": "Sende die Ablaufwarnung N Tage vor dem Ablaufdatum.",
"webhook_help": "In HA: Einstellungen → Automationen → Automation erstellen → Auslöser: Webhook → ID kopieren.",
"notify_title": "Push-Benachrichtigungen",
"notify_hint": "Sende Push-Benachrichtigungen über einen Home Assistant notify-Dienst.",
"notify_service_label": "Notify-Dienst",
"notify_service_placeholder": "notify.mobile_app_mein_handy",
"notify_service_hint": "Name des HA-notify-Dienstes (z.B. notify.mobile_app_phone). Leer lassen zum Deaktivieren.",
"sensor_title": "REST-Sensoren",
"sensor_hint": "Zur configuration.yaml hinzufügen, um EverShelf-Sensoren in Home Assistant zu erstellen.",
"sensor_copy_btn": "YAML kopieren",
"sensor_copied": "YAML in die Zwischenablage kopiert!",
"save_btn": "HA-Einstellungen speichern",
"ha_hint": "Wenn du Home Assistant verwendest, nutze den Home Assistant-Tab für TTS, Webhooks und Sensoren."
} }
}, },
"expiry": { "expiry": {
+54
View File
@@ -868,6 +868,60 @@
"forecast_label": "Forecast low-stock products", "forecast_label": "Forecast low-stock products",
"auto_add_label": "Auto-add to list when", "auto_add_label": "Auto-add to list when",
"auto_add_suffix": "remaining in stock (0 = only when empty)" "auto_add_suffix": "remaining in stock (0 = only when empty)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Connect EverShelf to Home Assistant for automations, push notifications and REST sensors.",
"enabled": "Enable Home Assistant integration",
"connection_title": "Connection",
"url_label": "Home Assistant URL",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "Base URL of your Home Assistant instance (e.g. http://homeassistant.local:8123).",
"token_label": "Long-Lived Access Token",
"token_hint": "Generate from HA Profile → Security → Long-Lived Access Tokens.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token saved (hidden for security)",
"test_btn": "Test connection",
"test_ok": "Connected to {version}",
"test_fail": "Connection failed: {error}",
"test_bad_token": "HA reachable but token is invalid",
"testing": "Testing…",
"error_no_url": "Please enter the Home Assistant URL first.",
"tts_title": "TTS on Smart Speaker",
"tts_hint": "Read recipe steps aloud on a Home Assistant media player.",
"tts_entity_label": "Media player entity ID",
"tts_entity_placeholder": "media_player.living_room",
"tts_entity_hint": "Entity ID of the HA media player. Find it in HA: Developer Tools → States.",
"tts_platform_label": "TTS platform",
"tts_platform_speak": "tts.speak (recommended)",
"tts_platform_notify": "notify.* (notification service)",
"tts_apply_btn": "Apply HA preset to TTS tab",
"tts_apply_hint": "Pre-fills the TTS tab with the Home Assistant URL and token.",
"tts_preset_applied": "HA preset applied to TTS tab.",
"webhook_title": "Webhook Automations",
"webhook_hint": "Send data to Home Assistant when pantry events occur. Create an HA automation with a Webhook trigger and paste the generated ID here.",
"webhook_id_label": "Webhook ID",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID of the webhook created in HA. Copy from: HA → Settings → Automations → Create → Webhook Trigger.",
"webhook_events_label": "Notify on these events",
"event_expiry": "Expiring products (daily)",
"event_shopping": "Item added to shopping list",
"event_stock": "Stock level updated",
"expiry_days_label": "Expiry lead time (days)",
"expiry_days_hint": "Send the expiry alert N days before the expiry date.",
"webhook_help": "In HA: Settings → Automations → Create automation → Trigger: Webhook → copy the generated ID above.",
"notify_title": "Push Notifications",
"notify_hint": "Send push notifications to your phone via a Home Assistant notify service.",
"notify_service_label": "Notify service",
"notify_service_placeholder": "notify.mobile_app_my_phone",
"notify_service_hint": "HA notify service name (e.g. notify.mobile_app_phone). Leave empty to disable.",
"sensor_title": "REST Sensors",
"sensor_hint": "Add to configuration.yaml to create EverShelf sensors in Home Assistant.",
"sensor_copy_btn": "Copy YAML",
"sensor_copied": "YAML copied to clipboard!",
"save_btn": "Save HA settings",
"ha_hint": "If you use Home Assistant, use the Home Assistant tab to configure TTS, webhooks and sensors."
} }
}, },
"expiry": { "expiry": {
+54
View File
@@ -821,6 +821,60 @@
"forecast_label": "Previsión de productos por agotar", "forecast_label": "Previsión de productos por agotar",
"auto_add_label": "Añadir automáticamente cuando", "auto_add_label": "Añadir automáticamente cuando",
"auto_add_suffix": "restante en stock (0 = solo cuando se agota)" "auto_add_suffix": "restante en stock (0 = solo cuando se agota)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Conecta EverShelf a Home Assistant para automatizaciones, notificaciones push y sensores REST.",
"enabled": "Activar integración con Home Assistant",
"connection_title": "Conexión",
"url_label": "URL de Home Assistant",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "URL base de tu instancia de Home Assistant.",
"token_label": "Token de acceso de larga duración",
"token_hint": "Genera desde Perfil HA → Seguridad → Tokens de acceso de larga duración.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token guardado (oculto por seguridad)",
"test_btn": "Probar conexión",
"test_ok": "Conectado a {version}",
"test_fail": "Conexión fallida: {error}",
"test_bad_token": "HA accesible pero el token no es válido",
"testing": "Probando…",
"error_no_url": "Por favor, introduce primero la URL de Home Assistant.",
"tts_title": "TTS en altavoz inteligente",
"tts_hint": "Lee los pasos de la receta en un reproductor de medios de Home Assistant.",
"tts_entity_label": "Entity ID del reproductor multimedia",
"tts_entity_placeholder": "media_player.salon",
"tts_entity_hint": "ID de entidad del reproductor multimedia HA. Encuéntralo en HA: Herramientas para desarrolladores → Estados.",
"tts_platform_label": "Plataforma TTS",
"tts_platform_speak": "tts.speak (recomendado)",
"tts_platform_notify": "notify.* (servicio de notificaciones)",
"tts_apply_btn": "Aplicar preset HA a la pestaña TTS",
"tts_apply_hint": "Pre-rellena la pestaña TTS con la URL y el token de Home Assistant.",
"tts_preset_applied": "Preset HA aplicado a la pestaña TTS.",
"webhook_title": "Automatizaciones Webhook",
"webhook_hint": "Envía datos a Home Assistant cuando ocurren eventos en la despensa.",
"webhook_id_label": "ID de Webhook",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID del webhook creado en HA. Copia desde: HA → Ajustes → Automatizaciones → Crear → Disparador Webhook.",
"webhook_events_label": "Notificar en estos eventos",
"event_expiry": "Productos próximos a caducar (diario)",
"event_shopping": "Artículo añadido a la lista de compras",
"event_stock": "Nivel de stock actualizado",
"expiry_days_label": "Antelación de caducidad (días)",
"expiry_days_hint": "Enviar alerta de caducidad N días antes de la fecha.",
"webhook_help": "En HA: Ajustes → Automatizaciones → Crear automatización → Disparador: Webhook → copia el ID generado.",
"notify_title": "Notificaciones push",
"notify_hint": "Envía notificaciones push a tu teléfono mediante un servicio notify de Home Assistant.",
"notify_service_label": "Servicio notify",
"notify_service_placeholder": "notify.mobile_app_mi_telefono",
"notify_service_hint": "Nombre del servicio notify de HA. Déjalo vacío para desactivar.",
"sensor_title": "Sensores REST",
"sensor_hint": "Añade a configuration.yaml para crear sensores de EverShelf en Home Assistant.",
"sensor_copy_btn": "Copiar YAML",
"sensor_copied": "¡YAML copiado al portapapeles!",
"save_btn": "Guardar ajustes HA",
"ha_hint": "Si usas Home Assistant, utiliza la pestaña Home Assistant para configurar TTS, webhooks y sensores."
} }
}, },
"expiry": { "expiry": {
+54
View File
@@ -821,6 +821,60 @@
"forecast_label": "Prévision des produits bientôt épuisés", "forecast_label": "Prévision des produits bientôt épuisés",
"auto_add_label": "Ajouter automatiquement quand", "auto_add_label": "Ajouter automatiquement quand",
"auto_add_suffix": "restant en stock (0 = seulement quand épuisé)" "auto_add_suffix": "restant en stock (0 = seulement quand épuisé)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Connectez EverShelf à Home Assistant pour les automations, les notifications push et les capteurs REST.",
"enabled": "Activer l'intégration Home Assistant",
"connection_title": "Connexion",
"url_label": "URL Home Assistant",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "URL de base de votre instance Home Assistant.",
"token_label": "Jeton d'accès longue durée",
"token_hint": "Générez depuis Profil HA → Sécurité → Jetons d'accès longue durée.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Jeton enregistré (masqué pour des raisons de sécurité)",
"test_btn": "Tester la connexion",
"test_ok": "Connecté à {version}",
"test_fail": "Connexion échouée : {error}",
"test_bad_token": "HA accessible mais le jeton est invalide",
"testing": "Test en cours…",
"error_no_url": "Veuillez d'abord saisir l'URL de Home Assistant.",
"tts_title": "TTS sur enceinte connectée",
"tts_hint": "Lisez les étapes de recette sur un media player Home Assistant.",
"tts_entity_label": "Entity ID du lecteur multimédia",
"tts_entity_placeholder": "media_player.salon",
"tts_entity_hint": "Entity ID du lecteur multimédia HA. Disponible dans HA : Outils développeur → États.",
"tts_platform_label": "Plateforme TTS",
"tts_platform_speak": "tts.speak (recommandé)",
"tts_platform_notify": "notify.* (service de notification)",
"tts_apply_btn": "Appliquer le preset HA à l'onglet TTS",
"tts_apply_hint": "Pré-remplit l'onglet TTS avec l'URL et le jeton de Home Assistant.",
"tts_preset_applied": "Preset HA appliqué à l'onglet TTS.",
"webhook_title": "Automations Webhook",
"webhook_hint": "Envoyez des données à Home Assistant lors d'événements dans le garde-manger.",
"webhook_id_label": "ID Webhook",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID du webhook créé dans HA. Copiez depuis : HA → Paramètres → Automations → Créer → Déclencheur Webhook.",
"webhook_events_label": "Notifier pour ces événements",
"event_expiry": "Produits expirant bientôt (quotidien)",
"event_shopping": "Article ajouté à la liste de courses",
"event_stock": "Niveau de stock mis à jour",
"expiry_days_label": "Préavis d'expiration (jours)",
"expiry_days_hint": "Envoyer l'alerte d'expiration N jours avant la date d'expiration.",
"webhook_help": "Dans HA : Paramètres → Automations → Créer → Déclencheur : Webhook → copier l'ID généré.",
"notify_title": "Notifications push",
"notify_hint": "Envoyez des notifications push sur votre téléphone via un service notify de Home Assistant.",
"notify_service_label": "Service notify",
"notify_service_placeholder": "notify.mobile_app_mon_telephone",
"notify_service_hint": "Nom du service notify HA. Laissez vide pour désactiver.",
"sensor_title": "Capteurs REST",
"sensor_hint": "Ajoutez à configuration.yaml pour créer des capteurs EverShelf dans Home Assistant.",
"sensor_copy_btn": "Copier le YAML",
"sensor_copied": "YAML copié dans le presse-papiers !",
"save_btn": "Enregistrer les paramètres HA",
"ha_hint": "Si vous utilisez Home Assistant, utilisez l'onglet Home Assistant pour configurer TTS, webhooks et capteurs."
} }
}, },
"expiry": { "expiry": {
+54
View File
@@ -868,6 +868,60 @@
"forecast_label": "Previsione prodotti in esaurimento", "forecast_label": "Previsione prodotti in esaurimento",
"auto_add_label": "Aggiungi automaticamente quando", "auto_add_label": "Aggiungi automaticamente quando",
"auto_add_suffix": "rimasto in magazzino (0 = solo quando esaurito)" "auto_add_suffix": "rimasto in magazzino (0 = solo quando esaurito)"
},
"ha": {
"tab": "Home Assistant",
"title": "Home Assistant",
"hint": "Collega EverShelf a Home Assistant per automazioni, notifiche push e sensori REST.",
"enabled": "Abilita integrazione Home Assistant",
"connection_title": "Connessione",
"url_label": "URL Home Assistant",
"url_placeholder": "http://192.168.1.50:8123",
"url_hint": "URL del tuo server Home Assistant (es. http://homeassistant.local:8123).",
"token_label": "Long-Lived Access Token",
"token_hint": "Genera da Profilo HA → Sicurezza → Token di accesso a lungo termine.",
"token_placeholder": "eyJhbGci...",
"token_saved": "Token salvato (non mostrato per sicurezza)",
"test_btn": "Testa connessione",
"test_ok": "Connesso a {version}",
"test_fail": "Connessione fallita: {error}",
"test_bad_token": "HA raggiungibile ma token non valido",
"testing": "Test in corso…",
"error_no_url": "Inserisci prima l'URL di Home Assistant.",
"tts_title": "TTS su Speaker Smart",
"tts_hint": "Leggi i passi delle ricette su un media player di Home Assistant.",
"tts_entity_label": "Entity ID media player",
"tts_entity_placeholder": "media_player.living_room",
"tts_entity_hint": "Entity ID del media player su cui vuoi la voce. Puoi trovarlo in HA: Strumenti per sviluppatori → Stati.",
"tts_platform_label": "Piattaforma TTS",
"tts_platform_speak": "tts.speak (raccomandato)",
"tts_platform_notify": "notify.* (servizio notifiche)",
"tts_apply_btn": "Applica preset HA al tab TTS",
"tts_apply_hint": "Pre-compila il tab TTS con l'URL e il token di Home Assistant.",
"tts_preset_applied": "Preset HA applicato al tab TTS.",
"webhook_title": "Automazioni Webhook",
"webhook_hint": "Invia dati a Home Assistant quando avvengono eventi nella dispensa. Crea un'automazione in HA con trigger Webhook e copia l'ID generato.",
"webhook_id_label": "Webhook ID",
"webhook_id_placeholder": "evershelf_webhook_abc123",
"webhook_id_hint": "ID del webhook creato in HA. Copia da: HA → Impostazioni → Automazioni → Crea → Trigger Webhook.",
"webhook_events_label": "Notifica per questi eventi",
"event_expiry": "Prodotti in scadenza (giornaliero)",
"event_shopping": "Aggiunta alla lista della spesa",
"event_stock": "Aggiornamento scorte",
"expiry_days_label": "Anticipo scadenze (giorni)",
"expiry_days_hint": "Invia la notifica di scadenza N giorni prima della data di scadenza.",
"webhook_help": "In HA: Impostazioni → Automazioni → Crea automazione → Trigger: Webhook → copia l'ID generato qui sopra.",
"notify_title": "Notifiche Push",
"notify_hint": "Invia notifiche push al tuo telefono tramite il servizio notify di Home Assistant.",
"notify_service_label": "Servizio notify",
"notify_service_placeholder": "notify.mobile_app_mio_telefono",
"notify_service_hint": "Nome del servizio notify HA (es. notify.mobile_app_phone). Lascia vuoto per disabilitare.",
"sensor_title": "Sensori REST",
"sensor_hint": "Aggiungi a configuration.yaml per creare sensori EverShelf in Home Assistant.",
"sensor_copy_btn": "Copia YAML",
"sensor_copied": "YAML copiato negli appunti!",
"save_btn": "Salva impostazioni HA",
"ha_hint": "Se usi Home Assistant, usa il tab Home Assistant per configurare TTS, webhook e sensori."
} }
}, },
"expiry": { "expiry": {