feat: TTS generic API builder, remove HA refs, pre-fill credentials
This commit is contained in:
+99
-33
@@ -648,15 +648,38 @@ async function loadSettingsUI() {
|
|||||||
`<span class="mplan-badge" style="opacity:0.85">${t.icon} ${t.label}</span>`
|
`<span class="mplan-badge" style="opacity:0.85">${t.icon} ${t.label}</span>`
|
||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
// TTS settings
|
// TTS settings — init defaults on first load
|
||||||
|
if (!s._tts_initialized) {
|
||||||
|
s.tts_url = s.tts_url || 'http://192.168.1.133:8123/api/events/noemi_speak';
|
||||||
|
s.tts_token = s.tts_token || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2OGQ3Njk1N2E0MjY0ZTBjOWQ4YjczZDY4ZDVmMWJlZCIsImlhdCI6MTc2MDI1NjIxNSwiZXhwIjoyMDc1NjE2MjE1fQ.X5UyMMPd7wTA6Gh11Nzg7Ox-enlDDom_lJIAJruUtcE';
|
||||||
|
s.tts_payload_key = s.tts_payload_key || 'message';
|
||||||
|
s.tts_method = s.tts_method || 'POST';
|
||||||
|
s.tts_auth_type = s.tts_auth_type || 'bearer';
|
||||||
|
s.tts_content_type = s.tts_content_type || 'application/json';
|
||||||
|
s.tts_enabled = s.tts_enabled !== undefined ? s.tts_enabled : true;
|
||||||
|
s._tts_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 ttsUrlEl = document.getElementById('setting-tts-url');
|
const ttsUrlEl = document.getElementById('setting-tts-url');
|
||||||
if (ttsUrlEl) ttsUrlEl.value = s.tts_url || 'http://192.168.1.133:8123/api/events/noemi_speak';
|
if (ttsUrlEl) ttsUrlEl.value = s.tts_url || '';
|
||||||
|
const ttsMethEl = document.getElementById('setting-tts-method');
|
||||||
|
if (ttsMethEl) ttsMethEl.value = s.tts_method || 'POST';
|
||||||
|
const ttsAuthTypeEl = document.getElementById('setting-tts-auth-type');
|
||||||
|
if (ttsAuthTypeEl) { ttsAuthTypeEl.value = s.tts_auth_type || 'bearer'; onTtsAuthTypeChange(ttsAuthTypeEl.value); }
|
||||||
const ttsTokenEl = document.getElementById('setting-tts-token');
|
const ttsTokenEl = document.getElementById('setting-tts-token');
|
||||||
if (ttsTokenEl) ttsTokenEl.value = s.tts_token || '';
|
if (ttsTokenEl) ttsTokenEl.value = s.tts_token || '';
|
||||||
|
const ttsAuthHdrNameEl = document.getElementById('setting-tts-auth-header-name');
|
||||||
|
if (ttsAuthHdrNameEl) ttsAuthHdrNameEl.value = s.tts_auth_header_name || '';
|
||||||
|
const ttsAuthHdrValEl = document.getElementById('setting-tts-auth-header-value');
|
||||||
|
if (ttsAuthHdrValEl) ttsAuthHdrValEl.value = s.tts_auth_header_value || '';
|
||||||
|
const ttsCtEl = document.getElementById('setting-tts-content-type');
|
||||||
|
if (ttsCtEl) ttsCtEl.value = s.tts_content_type || 'application/json';
|
||||||
const ttsPayloadKeyEl = document.getElementById('setting-tts-payload-key');
|
const ttsPayloadKeyEl = document.getElementById('setting-tts-payload-key');
|
||||||
if (ttsPayloadKeyEl) ttsPayloadKeyEl.value = s.tts_payload_key || 'message';
|
if (ttsPayloadKeyEl) ttsPayloadKeyEl.value = s.tts_payload_key || 'message';
|
||||||
|
const ttsExtraEl = document.getElementById('setting-tts-extra-fields');
|
||||||
|
if (ttsExtraEl) ttsExtraEl.value = s.tts_extra_fields || '';
|
||||||
|
|
||||||
// Load server-side settings if not already set locally
|
// Load server-side settings if not already set locally
|
||||||
try {
|
try {
|
||||||
@@ -760,12 +783,24 @@ async function saveSettings() {
|
|||||||
// TTS settings
|
// TTS settings
|
||||||
const ttsEnabledEl = document.getElementById('setting-tts-enabled');
|
const ttsEnabledEl = document.getElementById('setting-tts-enabled');
|
||||||
if (ttsEnabledEl) s.tts_enabled = ttsEnabledEl.checked;
|
if (ttsEnabledEl) s.tts_enabled = ttsEnabledEl.checked;
|
||||||
const ttsUrlEl = document.getElementById('setting-tts-url');
|
const ttsUrlEl2 = document.getElementById('setting-tts-url');
|
||||||
if (ttsUrlEl) s.tts_url = ttsUrlEl.value.trim();
|
if (ttsUrlEl2) s.tts_url = ttsUrlEl2.value.trim();
|
||||||
const ttsTokenEl = document.getElementById('setting-tts-token');
|
const ttsMethEl2 = document.getElementById('setting-tts-method');
|
||||||
if (ttsTokenEl) s.tts_token = ttsTokenEl.value.trim();
|
if (ttsMethEl2) s.tts_method = ttsMethEl2.value;
|
||||||
const ttsPayloadKeyEl = document.getElementById('setting-tts-payload-key');
|
const ttsAuthTypeEl2 = document.getElementById('setting-tts-auth-type');
|
||||||
if (ttsPayloadKeyEl) s.tts_payload_key = ttsPayloadKeyEl.value.trim() || 'message';
|
if (ttsAuthTypeEl2) s.tts_auth_type = ttsAuthTypeEl2.value;
|
||||||
|
const ttsTokenEl2 = document.getElementById('setting-tts-token');
|
||||||
|
if (ttsTokenEl2) s.tts_token = ttsTokenEl2.value.trim();
|
||||||
|
const ttsAuthHdrNameEl2 = document.getElementById('setting-tts-auth-header-name');
|
||||||
|
if (ttsAuthHdrNameEl2) s.tts_auth_header_name = ttsAuthHdrNameEl2.value.trim();
|
||||||
|
const ttsAuthHdrValEl2 = document.getElementById('setting-tts-auth-header-value');
|
||||||
|
if (ttsAuthHdrValEl2) s.tts_auth_header_value = ttsAuthHdrValEl2.value.trim();
|
||||||
|
const ttsCtEl2 = document.getElementById('setting-tts-content-type');
|
||||||
|
if (ttsCtEl2) s.tts_content_type = ttsCtEl2.value;
|
||||||
|
const ttsPayloadKeyEl2 = document.getElementById('setting-tts-payload-key');
|
||||||
|
if (ttsPayloadKeyEl2) s.tts_payload_key = ttsPayloadKeyEl2.value.trim() || 'message';
|
||||||
|
const ttsExtraEl2 = document.getElementById('setting-tts-extra-fields');
|
||||||
|
if (ttsExtraEl2) s.tts_extra_fields = ttsExtraEl2.value.trim();
|
||||||
// Save spesa AI prompt if the field exists
|
// Save spesa AI prompt if the field exists
|
||||||
const spesaPromptEl = document.getElementById('setting-spesa-ai-prompt');
|
const spesaPromptEl = document.getElementById('setting-spesa-ai-prompt');
|
||||||
if (spesaPromptEl) s.spesa_ai_prompt = spesaPromptEl.value.trim();
|
if (spesaPromptEl) s.spesa_ai_prompt = spesaPromptEl.value.trim();
|
||||||
@@ -6593,22 +6628,40 @@ function renderCookingStep() {
|
|||||||
if (_cookingTTS) speakCookingStep(cleanStep);
|
if (_cookingTTS) speakCookingStep(cleanStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _buildTtsRequest(text, s) {
|
||||||
|
const url = s.tts_url || 'http://192.168.1.133:8123/api/events/noemi_speak';
|
||||||
|
const method = s.tts_method || 'POST';
|
||||||
|
const authType = s.tts_auth_type || 'bearer';
|
||||||
|
const token = s.tts_token || '';
|
||||||
|
const payloadKey = s.tts_payload_key || 'message';
|
||||||
|
const contentType = s.tts_content_type || 'application/json';
|
||||||
|
let extraFields = {};
|
||||||
|
try { extraFields = JSON.parse(s.tts_extra_fields || '{}'); } catch(e) { /* invalid JSON, ignore */ }
|
||||||
|
const headers = { 'Content-Type': contentType };
|
||||||
|
if (authType === 'bearer' && token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
} else if (authType === 'header' && s.tts_auth_header_name) {
|
||||||
|
headers[s.tts_auth_header_name] = s.tts_auth_header_value || '';
|
||||||
|
}
|
||||||
|
const payload = { [payloadKey]: text, ...extraFields };
|
||||||
|
let body;
|
||||||
|
if (contentType === 'application/json') {
|
||||||
|
body = JSON.stringify(payload);
|
||||||
|
} else if (contentType === 'application/x-www-form-urlencoded') {
|
||||||
|
body = new URLSearchParams(Object.entries(payload).map(([k, v]) => [k, String(v)])).toString();
|
||||||
|
} else {
|
||||||
|
body = text;
|
||||||
|
}
|
||||||
|
return { url, method, headers, body };
|
||||||
|
}
|
||||||
|
|
||||||
async function speakCookingStep(text) {
|
async function speakCookingStep(text) {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
const s = getSettings();
|
const s = getSettings();
|
||||||
if (!s.tts_enabled) return;
|
if (!s.tts_enabled) return;
|
||||||
const url = s.tts_url || 'http://192.168.1.133:8123/api/events/noemi_speak';
|
|
||||||
const token = s.tts_token || '';
|
|
||||||
const payloadKey = s.tts_payload_key || 'message';
|
|
||||||
try {
|
try {
|
||||||
await fetch(url, {
|
const req = _buildTtsRequest(text, s);
|
||||||
method: 'POST',
|
await fetch(req.url, { method: req.method, headers: req.headers, body: req.body });
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ [payloadKey]: text })
|
|
||||||
});
|
|
||||||
} catch(e) { /* silent — TTS is non-critical */ }
|
} catch(e) { /* silent — TTS is non-critical */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6619,33 +6672,46 @@ function replayCookingTTS() {
|
|||||||
if (text) speakCookingStep(text);
|
if (text) speakCookingStep(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onTtsAuthTypeChange(type) {
|
||||||
|
const tokenGroup = document.getElementById('tts-token-group');
|
||||||
|
const headerGroup = document.getElementById('tts-custom-header-group');
|
||||||
|
if (tokenGroup) tokenGroup.style.display = type === 'bearer' ? '' : 'none';
|
||||||
|
if (headerGroup) headerGroup.style.display = type === 'header' ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
async function testTTS() {
|
async function testTTS() {
|
||||||
const statusEl = document.getElementById('tts-test-status');
|
const statusEl = document.getElementById('tts-test-status');
|
||||||
// Read values directly from the form (not yet saved settings)
|
// Build settings from current form values (before saving)
|
||||||
|
let extraFields = {};
|
||||||
|
try { extraFields = JSON.parse((document.getElementById('setting-tts-extra-fields')?.value || '{}').trim() || '{}'); } catch(e) { /* ignore */ }
|
||||||
|
const formSettings = {
|
||||||
|
tts_url: (document.getElementById('setting-tts-url')?.value || '').trim(),
|
||||||
|
tts_method: document.getElementById('setting-tts-method')?.value || 'POST',
|
||||||
|
tts_auth_type: document.getElementById('setting-tts-auth-type')?.value || 'bearer',
|
||||||
|
tts_token: (document.getElementById('setting-tts-token')?.value || '').trim(),
|
||||||
|
tts_auth_header_name: (document.getElementById('setting-tts-auth-header-name')?.value || '').trim(),
|
||||||
|
tts_auth_header_value: (document.getElementById('setting-tts-auth-header-value')?.value || '').trim(),
|
||||||
|
tts_content_type: document.getElementById('setting-tts-content-type')?.value || 'application/json',
|
||||||
|
tts_payload_key: (document.getElementById('setting-tts-payload-key')?.value || '').trim() || 'message',
|
||||||
|
tts_extra_fields: document.getElementById('setting-tts-extra-fields')?.value || ''
|
||||||
|
};
|
||||||
const enabled = document.getElementById('setting-tts-enabled')?.checked;
|
const enabled = document.getElementById('setting-tts-enabled')?.checked;
|
||||||
const url = (document.getElementById('setting-tts-url')?.value || '').trim() || 'http://192.168.1.133:8123/api/events/noemi_speak';
|
|
||||||
const token = (document.getElementById('setting-tts-token')?.value || '').trim();
|
|
||||||
const payloadKey = (document.getElementById('setting-tts-payload-key')?.value || '').trim() || 'message';
|
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '⚠️ TTS non attivo — attiva il toggle prima di testare.'; }
|
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '⚠️ TTS non attivo — attiva il toggle prima di testare.'; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!token) {
|
if (!formSettings.tts_url) {
|
||||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '⚠️ Bearer Token mancante.'; }
|
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '⚠️ URL endpoint mancante.'; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status'; statusEl.textContent = '⏳ Invio in corso…'; }
|
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status'; statusEl.textContent = '⏳ Invio in corso…'; }
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const req = _buildTtsRequest('Test vocale Dispensa Manager', formSettings);
|
||||||
method: 'POST',
|
const res = await fetch(req.url, { method: req.method, headers: req.headers, body: req.body });
|
||||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ [payloadKey]: 'Test vocale Dispensa Manager' })
|
|
||||||
});
|
|
||||||
if (res.ok || res.status === 200) {
|
if (res.ok || res.status === 200) {
|
||||||
if (statusEl) { statusEl.className = 'settings-status success'; statusEl.textContent = '✅ Richiesta inviata! Controlla che il tuo altoparlante abbia parlato.'; }
|
if (statusEl) { statusEl.className = 'settings-status success'; statusEl.textContent = `✅ Risposta ${res.status} — controlla che l'altoparlante abbia parlato.`; }
|
||||||
} else {
|
} else {
|
||||||
if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = `⚠️ Risposta HTTP ${res.status}: ${res.statusText}`; }
|
if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = `⚠️ HTTP ${res.status}: ${res.statusText}`; }
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = `❌ Errore di rete: ${e.message}`; }
|
if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = `❌ Errore di rete: ${e.message}`; }
|
||||||
|
|||||||
@@ -967,3 +967,4 @@
|
|||||||
[2026-04-04 14:25:01] OK — 11 items cached
|
[2026-04-04 14:25:01] OK — 11 items cached
|
||||||
[2026-04-04 14:30:02] OK — 11 items cached
|
[2026-04-04 14:30:02] OK — 11 items cached
|
||||||
[2026-04-04 14:35:02] OK — 11 items cached
|
[2026-04-04 14:35:02] OK — 11 items cached
|
||||||
|
[2026-04-04 14:40:01] OK — 11 items cached
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"success":true,"items":[{"product_id":151,"name":"Arance Tarocco","brand":"","category":"frutta","unit":"pz","current_qty":0,"default_qty":0,"package_unit":"","pct_left":0,"use_count":9,"buy_count":2,"daily_rate":0.55,"uses_per_month":13.6,"days_since_last_use":2,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (9x)"],"score":135,"on_bring":true,"locations":""},{"product_id":70,"name":"Latte Parzialmente Scremato Uht","brand":"Latteria","category":"bevande","unit":"conf","current_qty":0,"default_qty":500,"package_unit":"ml","pct_left":0,"use_count":13,"buy_count":3,"daily_rate":6.74,"uses_per_month":15.6,"days_since_last_use":9,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (13x)"],"score":135,"on_bring":true,"locations":""},{"product_id":129,"name":"Latte di Montagna","brand":"Mila","category":"en:dairies","unit":"conf","current_qty":0,"default_qty":1000,"package_unit":"ml","pct_left":0,"use_count":9,"buy_count":3,"daily_rate":0.2,"uses_per_month":13.6,"days_since_last_use":1,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (9x)"],"score":135,"on_bring":true,"locations":""},{"product_id":47,"name":"Lenticchie","brand":"Primia","category":"en:plant-based-foods-and-beverages","unit":"g","current_qty":0,"default_qty":400,"package_unit":"","pct_left":0,"use_count":5,"buy_count":3,"daily_rate":35.88,"uses_per_month":6,"days_since_last_use":0,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (5x)"],"score":130,"on_bring":true,"locations":""},{"product_id":115,"name":"Aglio rosso","brand":"Duoccio","category":"condimenti","unit":"pz","current_qty":0,"default_qty":0,"package_unit":"","pct_left":0,"use_count":2,"buy_count":2,"daily_rate":0.14,"uses_per_month":2.7,"days_since_last_use":15,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito"],"score":100,"on_bring":true,"locations":""},{"product_id":152,"name":"Muesli Frutta Secca","brand":"Crownfield","category":"altro","unit":"g","current_qty":0,"default_qty":750,"package_unit":"","pct_left":0,"use_count":4,"buy_count":2,"daily_rate":34.01,"uses_per_month":6.9,"days_since_last_use":9,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito"],"score":100,"on_bring":false,"locations":""},{"product_id":3,"name":"Cracker integrali","brand":"Barilla,Mulino Bianco","category":"en:snacks","unit":"conf","current_qty":2,"default_qty":25,"package_unit":"g","pct_left":12,"use_count":6,"buy_count":1,"daily_rate":0.56,"uses_per_month":7.2,"days_since_last_use":6,"days_left":4,"expiry_date":"2026-04-28","days_to_expiry":23,"is_opened":true,"urgency":"high","reasons":["Quasi finito (12%)"],"score":90,"on_bring":true,"locations":"dispensa"},{"product_id":132,"name":"Noci sgusciate","brand":"Fruttbella","category":"conserve","unit":"g","current_qty":60,"default_qty":200,"package_unit":"","pct_left":30,"use_count":4,"buy_count":1,"daily_rate":7.04,"uses_per_month":6,"days_since_last_use":0,"days_left":9,"expiry_date":"2026-04-29","days_to_expiry":24,"is_opened":true,"urgency":"medium","reasons":["Finisce tra ~9gg"],"score":50,"on_bring":true,"locations":"dispensa"},{"product_id":69,"name":"Cipolla Dorata","brand":"","category":"verdura","unit":"pz","current_qty":4,"default_qty":0,"package_unit":"","pct_left":44,"use_count":12,"buy_count":1,"daily_rate":0.32,"uses_per_month":14.4,"days_since_last_use":0,"days_left":12,"expiry_date":"2026-04-13","days_to_expiry":8,"is_opened":false,"urgency":"low","reasons":["Previsto esaurimento tra ~12gg"],"score":40,"on_bring":true,"locations":"frigo"},{"product_id":154,"name":"Mela Rossa","brand":"","category":"frutta","unit":"pz","current_qty":7,"default_qty":1,"package_unit":"","pct_left":32,"use_count":5,"buy_count":1,"daily_rate":0.87,"uses_per_month":8.7,"days_since_last_use":0,"days_left":8,"expiry_date":"2026-04-15","days_to_expiry":10,"is_opened":false,"urgency":"low","reasons":["Previsto esaurimento tra ~8gg"],"score":35,"on_bring":false,"locations":"frigo"},{"product_id":136,"name":"Biscotti Pastefrolle","brand":"Balocco","category":"snack","unit":"g","current_qty":350,"default_qty":700,"package_unit":"","pct_left":67,"use_count":4,"buy_count":2,"daily_rate":35.22,"uses_per_month":6,"days_since_last_use":0,"days_left":10,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"low","reasons":["Previsto esaurimento tra ~10gg"],"score":25,"on_bring":false,"locations":"dispensa"}],"cached_at":"2026-04-04T14:35:02+00:00","cached_ts":1775313302}
|
{"success":true,"items":[{"product_id":151,"name":"Arance Tarocco","brand":"","category":"frutta","unit":"pz","current_qty":0,"default_qty":0,"package_unit":"","pct_left":0,"use_count":9,"buy_count":2,"daily_rate":0.55,"uses_per_month":13.6,"days_since_last_use":2,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (9x)"],"score":135,"on_bring":true,"locations":""},{"product_id":70,"name":"Latte Parzialmente Scremato Uht","brand":"Latteria","category":"bevande","unit":"conf","current_qty":0,"default_qty":500,"package_unit":"ml","pct_left":0,"use_count":13,"buy_count":3,"daily_rate":6.74,"uses_per_month":15.6,"days_since_last_use":9,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (13x)"],"score":135,"on_bring":true,"locations":""},{"product_id":129,"name":"Latte di Montagna","brand":"Mila","category":"en:dairies","unit":"conf","current_qty":0,"default_qty":1000,"package_unit":"ml","pct_left":0,"use_count":9,"buy_count":3,"daily_rate":0.2,"uses_per_month":13.6,"days_since_last_use":1,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (9x)"],"score":135,"on_bring":true,"locations":""},{"product_id":47,"name":"Lenticchie","brand":"Primia","category":"en:plant-based-foods-and-beverages","unit":"g","current_qty":0,"default_qty":400,"package_unit":"","pct_left":0,"use_count":5,"buy_count":3,"daily_rate":35.87,"uses_per_month":6,"days_since_last_use":0,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito","Uso frequente (5x)"],"score":130,"on_bring":true,"locations":""},{"product_id":115,"name":"Aglio rosso","brand":"Duoccio","category":"condimenti","unit":"pz","current_qty":0,"default_qty":0,"package_unit":"","pct_left":0,"use_count":2,"buy_count":2,"daily_rate":0.14,"uses_per_month":2.7,"days_since_last_use":15,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito"],"score":100,"on_bring":true,"locations":""},{"product_id":152,"name":"Muesli Frutta Secca","brand":"Crownfield","category":"altro","unit":"g","current_qty":0,"default_qty":750,"package_unit":"","pct_left":0,"use_count":4,"buy_count":2,"daily_rate":34,"uses_per_month":6.9,"days_since_last_use":9,"days_left":0,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"critical","reasons":["Esaurito"],"score":100,"on_bring":false,"locations":""},{"product_id":3,"name":"Cracker integrali","brand":"Barilla,Mulino Bianco","category":"en:snacks","unit":"conf","current_qty":2,"default_qty":25,"package_unit":"g","pct_left":12,"use_count":6,"buy_count":1,"daily_rate":0.56,"uses_per_month":7.2,"days_since_last_use":6,"days_left":4,"expiry_date":"2026-04-28","days_to_expiry":23,"is_opened":true,"urgency":"high","reasons":["Quasi finito (12%)"],"score":90,"on_bring":true,"locations":"dispensa"},{"product_id":132,"name":"Noci sgusciate","brand":"Fruttbella","category":"conserve","unit":"g","current_qty":60,"default_qty":200,"package_unit":"","pct_left":30,"use_count":4,"buy_count":1,"daily_rate":7.04,"uses_per_month":6,"days_since_last_use":0,"days_left":9,"expiry_date":"2026-04-29","days_to_expiry":24,"is_opened":true,"urgency":"medium","reasons":["Finisce tra ~9gg"],"score":50,"on_bring":true,"locations":"dispensa"},{"product_id":69,"name":"Cipolla Dorata","brand":"","category":"verdura","unit":"pz","current_qty":4,"default_qty":0,"package_unit":"","pct_left":44,"use_count":12,"buy_count":1,"daily_rate":0.32,"uses_per_month":14.4,"days_since_last_use":0,"days_left":12,"expiry_date":"2026-04-13","days_to_expiry":8,"is_opened":false,"urgency":"low","reasons":["Previsto esaurimento tra ~12gg"],"score":40,"on_bring":true,"locations":"frigo"},{"product_id":154,"name":"Mela Rossa","brand":"","category":"frutta","unit":"pz","current_qty":7,"default_qty":1,"package_unit":"","pct_left":32,"use_count":5,"buy_count":1,"daily_rate":0.87,"uses_per_month":8.7,"days_since_last_use":0,"days_left":8,"expiry_date":"2026-04-15","days_to_expiry":10,"is_opened":false,"urgency":"low","reasons":["Previsto esaurimento tra ~8gg"],"score":35,"on_bring":false,"locations":"frigo"},{"product_id":136,"name":"Biscotti Pastefrolle","brand":"Balocco","category":"snack","unit":"g","current_qty":350,"default_qty":700,"package_unit":"","pct_left":67,"use_count":4,"buy_count":2,"daily_rate":35.22,"uses_per_month":6,"days_since_last_use":0,"days_left":10,"expiry_date":null,"days_to_expiry":999,"is_opened":false,"urgency":"low","reasons":["Previsto esaurimento tra ~10gg"],"score":25,"on_bring":false,"locations":"dispensa"}],"cached_at":"2026-04-04T14:40:01+00:00","cached_ts":1775313601}
|
||||||
+46
-6
@@ -850,10 +850,10 @@
|
|||||||
<div class="settings-panel" id="tab-tts">
|
<div class="settings-panel" id="tab-tts">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>🔊 Voce & TTS</h4>
|
<h4>🔊 Voce & TTS</h4>
|
||||||
<p class="settings-hint">Configura la sintesi vocale tramite Home Assistant. I passi della ricetta e i timer verranno letti ad alta voce attraverso l'altoparlante configurato.</p>
|
<p class="settings-hint">Configura la sintesi vocale tramite qualsiasi API REST esterna. I passi della ricetta e i timer scaduti verranno inviati all'endpoint configurato.</p>
|
||||||
<div class="form-group" style="margin-bottom:10px">
|
<div class="form-group" style="margin-bottom:10px">
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<span>✅ Attiva TTS via Home Assistant</span>
|
<span>✅ Attiva TTS</span>
|
||||||
<span class="toggle-switch">
|
<span class="toggle-switch">
|
||||||
<input type="checkbox" id="setting-tts-enabled">
|
<input type="checkbox" id="setting-tts-enabled">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
@@ -861,18 +861,58 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>🌐 URL Evento</label>
|
<label>🌐 URL Endpoint</label>
|
||||||
<input type="url" id="setting-tts-url" class="form-input" placeholder="http://192.168.1.133:8123/api/events/noemi_speak">
|
<input type="url" id="setting-tts-url" class="form-input" placeholder="https://...">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<label>📡 Metodo HTTP</label>
|
||||||
|
<select id="setting-tts-method" class="form-input">
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>🔐 Autenticazione</label>
|
||||||
|
<select id="setting-tts-auth-type" class="form-input" onchange="onTtsAuthTypeChange(this.value)">
|
||||||
|
<option value="bearer">Bearer Token</option>
|
||||||
|
<option value="header">Header personalizzato</option>
|
||||||
|
<option value="none">Nessuna</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="tts-token-group">
|
||||||
<label>🔑 Bearer Token</label>
|
<label>🔑 Bearer Token</label>
|
||||||
<input type="password" id="setting-tts-token" class="form-input" placeholder="eyJhbGci...">
|
<input type="password" id="setting-tts-token" class="form-input" placeholder="eyJhbGci...">
|
||||||
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-tts-token')">👁️ Mostra/Nascondi</button>
|
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-tts-token')">👁️ Mostra/Nascondi</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tts-custom-header-group" style="display:none">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>📋 Nome header</label>
|
||||||
|
<input type="text" id="setting-tts-auth-header-name" class="form-input" placeholder="X-API-Key">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>📋 Valore header</label>
|
||||||
|
<input type="text" id="setting-tts-auth-header-value" class="form-input" placeholder="...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>🗝️ Chiave payload</label>
|
<label>📄 Content-Type</label>
|
||||||
|
<select id="setting-tts-content-type" class="form-input">
|
||||||
|
<option value="application/json">application/json</option>
|
||||||
|
<option value="application/x-www-form-urlencoded">application/x-www-form-urlencoded</option>
|
||||||
|
<option value="text/plain">text/plain</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>🗝️ Campo testo nel payload</label>
|
||||||
<input type="text" id="setting-tts-payload-key" class="form-input" placeholder="message">
|
<input type="text" id="setting-tts-payload-key" class="form-input" placeholder="message">
|
||||||
<p class="settings-hint">Nome del campo JSON inviato all'evento (default: <code>message</code>).</p>
|
<p class="settings-hint">Nome del campo JSON che conterrà il testo da leggere (es: <code>message</code>, <code>text</code>).</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>➕ Campi extra (JSON)</label>
|
||||||
|
<textarea id="setting-tts-extra-fields" class="form-input" rows="3" placeholder='{"entity_id": "media_player.living_room"}'></textarea>
|
||||||
|
<p class="settings-hint">Campi aggiuntivi da includere nel payload, in formato JSON. Lascia vuoto se non necessario.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()">🔊 Invia Test Vocale</button>
|
<button class="btn btn-large btn-accent full-width mt-2" onclick="testTTS()">🔊 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user