From 2afd0295b41cc8a302c00af197f2cb2a9487948e Mon Sep 17 00:00:00 2001 From: Joseph Abbey Date: Thu, 25 Jan 2024 10:40:27 +0000 Subject: [PATCH] Error handling --- web/index.html | 35 ++- web/main.js | 599 ++++++++++++++++++++++++++++++------------------- 2 files changed, 389 insertions(+), 245 deletions(-) diff --git a/web/index.html b/web/index.html index eee68d0..8db9a99 100644 --- a/web/index.html +++ b/web/index.html @@ -121,6 +121,11 @@ border-radius: 0.25em; padding: 0.25em; + &::placeholder { + color: var(--ctp-mocha-text); + opacity: 0.4; + } + &:focus-visible { outline: none; border: 1px solid var(--ctp-mocha-teal); @@ -169,14 +174,19 @@ button.icon { border: none; - padding: 0; + padding: 0.1em; margin: 0; - width: 1.5em; - height: 1.5em; - background-color: transparent; + width: 1.8em; + height: 1.8em; + background-color: var(--ctp-mocha-surface1); + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } &:hover { - background-color: var(--ctp-mocha-red); + background-color: var(--ctp-mocha-overlay1); } &::before { @@ -191,8 +201,16 @@ margin: 0; padding: 0; } - &[icon='close']::before { - background-image: url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/close/default/48px.svg); + &[icon='close'] { + &:hover { + background-color: var(--ctp-mocha-red); + } + &::before { + background-image: url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/close/default/48px.svg); + } + } + &[icon='download']::before { + background-image: url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/download/default/48px.svg); } } } @@ -212,7 +230,8 @@ type="url" name="menu_url" id="menu_url" - pattern="https://.*" /> + pattern="https://.*\.json" /> + >} [id, name] */ -async function get_entities(api_url, api_token) { - const res = await fetch(api_url + '/template', { - method: 'POST', - headers: { - Authorization: `Bearer ${api_token}`, - }, - mode: 'cors', - body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, - }); - if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); +async function get_entities() { + try { + const res = await fetch(api_url + '/template', { + method: 'POST', + headers: { + Authorization: `Bearer ${api_token}`, + }, + mode: 'cors', + body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, + }); + if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + return {}; + } + document.querySelector('#api_url').classList.remove('invalid'); + document.querySelector('#api_token').classList.remove('invalid'); + return Object.fromEntries(await res.json()); + } catch { + document.querySelector('#api_url').classList.add('invalid'); + return {}; } - return Object.fromEntries(await res.json()); } /** * Get all devices in HomeAssistant. - * @param {string} api_url - * @param {string} api_token * @returns {Promise>} [id, name] */ -async function get_devices(api_url, api_token) { - const res = await fetch(api_url + '/template', { - method: 'POST', - headers: { - Authorization: `Bearer ${api_token}`, - }, - mode: 'cors', - body: `{"template":"{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq', None) | list %}[{% for device in devices %}[\\"{{ device }}\\",\\"{{ device_attr(device, 'name') }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, - }); - if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); +async function get_devices() { + try { + const res = await fetch(api_url + '/template', { + method: 'POST', + headers: { + Authorization: `Bearer ${api_token}`, + }, + mode: 'cors', + body: `{"template":"{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq', None) | list %}[{% for device in devices %}[\\"{{ device }}\\",\\"{{ device_attr(device, 'name') }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, + }); + if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + return {}; + } + document.querySelector('#api_url').classList.remove('invalid'); + document.querySelector('#api_token').classList.remove('invalid'); + return Object.fromEntries(await res.json()); + } catch { + document.querySelector('#api_url').classList.add('invalid'); + return {}; } - return Object.fromEntries(await res.json()); } /** * Get all areas in HomeAssistant. - * @param {string} api_url - * @param {string} api_token * @returns {Promise>} [id, name] */ -async function get_areas(api_url, api_token) { - const res = await fetch(api_url + '/template', { - method: 'POST', - headers: { - Authorization: `Bearer ${api_token}`, - }, - mode: 'cors', - body: `{"template":"[{% for area in areas() %}[\\"{{ area }}\\",\\"{{ area_name(area) }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, - }); - if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); +async function get_areas() { + try { + const res = await fetch(api_url + '/template', { + method: 'POST', + headers: { + Authorization: `Bearer ${api_token}`, + }, + mode: 'cors', + body: `{"template":"[{% for area in areas() %}[\\"{{ area }}\\",\\"{{ area_name(area) }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`, + }); + if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + return {}; + } + document.querySelector('#api_url').classList.remove('invalid'); + document.querySelector('#api_token').classList.remove('invalid'); + return Object.fromEntries(await res.json()); + } catch { + document.querySelector('#api_url').classList.add('invalid'); + return {}; } - return Object.fromEntries(await res.json()); } /** * Get all services in HomeAssistant. - * @param {string} api_url - * @param {string} api_token * @returns {Promise<[string, { name: string; description: string; fields: Record }][]>} [id, data] */ -async function get_services(api_url, api_token) { - const res = await fetch(api_url + '/services', { - method: 'GET', - headers: { - Authorization: `Bearer ${api_token}`, - }, - mode: 'cors', - }); - if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); - } - const data = await res.json(); - const services = []; - for (const d of data) { - for (const service in d.services) { - services.push([`${d.domain}.${service}`, d.services[service]]); +async function get_services() { + try { + const res = await fetch(api_url + '/services', { + method: 'GET', + headers: { + Authorization: `Bearer ${api_token}`, + }, + mode: 'cors', + }); + if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + return {}; } + document.querySelector('#api_url').classList.remove('invalid'); + document.querySelector('#api_token').classList.remove('invalid'); + const data = await res.json(); + const services = []; + for (const d of data) { + for (const service in d.services) { + services.push([`${d.domain}.${service}`, d.services[service]]); + } + } + return services; + } catch { + document.querySelector('#api_url').classList.add('invalid'); + return []; } - return services; } /** @@ -343,69 +371,6 @@ async function generate_schema(entities, devices, areas, services, schema) { return schema; } -let api_url = localStorage.getItem('api_url') ?? ''; -let menu_url = localStorage.getItem('menu_url') ?? ''; -let api_token = localStorage.getItem('api_token') ?? ''; -document.querySelector('#api_url').value = api_url; -document.querySelector('#menu_url').value = menu_url; -document.querySelector('#api_token').value = api_token; - -document.querySelector('#troubleshooting').addEventListener('click', (e) => { - document.querySelector('#troubleshooting-dialog').showModal(); -}); - -document.querySelector('#test-api').addEventListener('click', async (e) => { - try { - document.querySelector('#test-api-response').innerText = 'Testing...'; - const res = await fetch(api_url + '/', { - headers: { - Authorization: `Bearer ${api_token}`, - }, - cache: 'no-cache', - mode: 'cors', - }); - const text = await res.text(); - if (res.status == 200) { - document.querySelector('#test-api-response').innerText = - JSON.parse(text).message; - } else if (res.status == 400) { - try { - document.querySelector('#test-api-response').innerText = - JSON.parse(text).message; - } catch { - document.querySelector('#test-api-response').innerText = text; - } - } else if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); - document.querySelector('#test-api-response').innerText = 'Invalid token.'; - } else { - document.querySelector('#test-api-response').innerText = text; - } - } catch (e) { - document.querySelector('#test-api-response').innerText = e.message; - } -}); -document.querySelector('#test-menu').addEventListener('click', async (e) => { - try { - document.querySelector('#test-menu-response').innerText = 'Testing...'; - const res = await fetch(menu_url, { - cache: 'no-cache', - mode: 'cors', - }); - if (res.status == 200) { - document.querySelector('#test-menu-response').innerText = 'Available'; - } else if (res.status == 400) { - document.querySelector('#test-menu-response').innerText = - await res.text(); - } else { - document.querySelector('#test-menu-response').innerText = - await res.text(); - } - } catch (e) { - document.querySelector('#test-menu-response').innerText = e.message; - } -}); - function get(d, p) { for (let i = 0; i < p.length; i++) { d = d[p[i]]; @@ -432,6 +397,38 @@ function toast({ text, color }) { return t; } +let entities, devices, areas, services, schema; +async function loadSchema() { + [entities, devices, areas, services, schema] = await Promise.all([ + get_entities(), + get_devices(), + get_areas(), + get_services(), + get_schema(), + ]); + if (window.makeMarkers) { + window.makeMarkers(); + } + try { + schema = await generate_schema(entities, devices, areas, services, schema); + } catch {} + console.log(schema); + if (window.m && window.modelUri) { + // configure the JSON language support with schemas and schema associations + window.m.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemas: [ + { + uri: 'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json', + fileMatch: [window.modelUri.toString()], + schema, + }, + ], + }); + } +} +loadSchema(); + // require is provided by loader.min.js. require.config({ paths: { @@ -439,26 +436,11 @@ require.config({ }, }); require(['vs/editor/editor.main'], async () => { - let entities, devices, areas, services, schema; - async function loadSchema() { - [entities, devices, areas, services, schema] = await Promise.all([ - get_entities(api_url, api_token), - get_devices(api_url, api_token), - get_areas(api_url, api_token), - get_services(api_url, api_token), - get_schema(), - ]); - makeMarkers(); - try { - schema = await generate_schema( - entities, - devices, - areas, - services, - schema - ); - } catch {} - console.log(schema); + window.m = monaco; + var modelUri = monaco.Uri.parse('/config/www/garmin/menu.json'); // a made up unique URI for our model + window.modelUri = modelUri; + + if (schema) { // configure the JSON language support with schemas and schema associations monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, @@ -472,6 +454,110 @@ require(['vs/editor/editor.main'], async () => { }); } + document.querySelector('#api_url').value = api_url; + document.querySelector('#menu_url').value = menu_url; + document.querySelector('#api_token').value = api_token; + + document.querySelector('#troubleshooting').addEventListener('click', (e) => { + document.querySelector('#troubleshooting-dialog').showModal(); + }); + + document.querySelector('#test-api').addEventListener('click', async (e) => { + try { + document.querySelector('#test-api-response').innerText = 'Testing...'; + const res = await fetch(api_url + '/', { + headers: { + Authorization: `Bearer ${api_token}`, + }, + cache: 'no-cache', + mode: 'cors', + }); + const text = await res.text(); + if (res.status == 200) { + document.querySelector('#test-api-response').innerText = + JSON.parse(text).message; + document.querySelector('#api_token').classList.remove('invalid'); + document.querySelector('#api_url').classList.remove('invalid'); + } else if (res.status == 400) { + document.querySelector('#api_url').classList.add('invalid'); + try { + document.querySelector('#test-api-response').innerText = + JSON.parse(text).message; + } catch { + document.querySelector('#test-api-response').innerText = text; + } + } else if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + document.querySelector('#test-api-response').innerText = + 'Invalid token.'; + } else { + document.querySelector('#test-api-response').innerText = text; + } + } catch (e) { + document.querySelector('#test-api-response').innerText = + 'Check CORS settings on HomeAssistant server.'; + document.querySelector('#api_token').classList.add('invalid'); + } + }); + document.querySelector('#test-menu').addEventListener('click', async (e) => { + try { + document.querySelector('#test-menu-response').innerText = 'Testing...'; + const res = await fetch(menu_url, { + cache: 'no-cache', + mode: 'cors', + }); + if (res.status == 200) { + document.querySelector('#menu_url').classList.remove('invalid'); + document.querySelector('#test-menu-response').innerText = 'Available'; + } else if (res.status == 400) { + document.querySelector('#menu_url').classList.add('invalid'); + document.querySelector('#test-menu-response').innerText = + await res.text(); + } else { + document.querySelector('#menu_url').classList.add('invalid'); + document.querySelector('#test-menu-response').innerText = + await res.text(); + } + } catch (e) { + document.querySelector('#menu_url').classList.add('invalid'); + document.querySelector('#test-menu-response').innerText = + 'Check CORS settings on HomeAssistant server.'; + } + }); + document.querySelector('#download').addEventListener('click', async (e) => { + try { + const t = toast({ + text: 'Downloading...', + }); + const res = await fetch(menu_url, { + cache: 'no-cache', + mode: 'cors', + }); + t.hideToast(); + if (res.status == 200) { + document.querySelector('#menu_url').classList.remove('invalid'); + const text = await res.text(); + model.setValue(text); + toast({ + text: 'Downloaded!', + color: 'var(--ctp-mocha-green)', + }); + } else { + document.querySelector('#menu_url').classList.add('invalid'); + toast({ + text: await res.text(), + color: 'var(--ctp-mocha-red)', + }); + } + } catch (e) { + toast({ + text: 'Check CORS settings on HomeAssistant server.', + color: 'var(--ctp-mocha-red)', + }); + document.querySelector('#menu_url').classList.add('invalid'); + } + }); + document.querySelector('#api_url').addEventListener('change', (e) => { api_url = e.target.value; localStorage.setItem('api_url', api_url); @@ -491,32 +577,40 @@ require(['vs/editor/editor.main'], async () => { document.querySelector('#test-api-response').innerText = 'Check now!'; loadSchema(); }); - loadSchema(); checkRemoteMenu(); async function checkRemoteMenu() { if (menu_url != '') { - const remote = await fetch(menu_url, { - cache: 'no-cache', - mode: 'cors', - }); - if (remote.status == 200) { - document.querySelector('#menu_url').classList.remove('invalid'); - const text = await remote.text(); - if (model.getValue() === text) { - document.querySelector('#menu_url').classList.remove('outofsync'); + try { + const remote = await fetch(menu_url, { + cache: 'no-cache', + mode: 'cors', + }); + if (remote.status == 200) { + document.querySelector('#menu_url').classList.remove('invalid'); + document.querySelector('#download').disabled = false; + const text = await remote.text(); + if (model.getValue() === text) { + document.querySelector('#menu_url').classList.remove('outofsync'); + } else { + document.querySelector('#menu_url').classList.add('outofsync'); + } } else { - document.querySelector('#menu_url').classList.add('outofsync'); + document.querySelector('#menu_url').classList.add('invalid'); + document.querySelector('#download').disabled = true; } - } else { + } catch { document.querySelector('#menu_url').classList.add('invalid'); + document.querySelector('#download').disabled = true; } + } else { + document.querySelector('#menu_url').classList.remove('invalid'); + document.querySelector('#download').disabled = true; } } - // Configures two JSON schemas, with references. + setInterval(checkRemoteMenu, 30000); - var modelUri = monaco.Uri.parse('/config/www/garmin/menu.json'); // a made up unique URI for our model var model = monaco.editor.createModel( localStorage.getItem('json') ?? '{}', 'json', @@ -556,32 +650,41 @@ require(['vs/editor/editor.main'], async () => { const t = toast({ text: 'Rendering template...', }); - const res = await fetch(api_url + '/template', { - method: 'POST', - headers: { - Authorization: `Bearer ${api_token}`, - }, - mode: 'cors', - body: `{"template":"${template}"}`, - }); - t.hideToast(); - if (res.status == 200) { - toast({ - text: await res.text(), - color: 'var(--ctp-mocha-green)', + try { + const res = await fetch(api_url + '/template', { + method: 'POST', + headers: { + Authorization: `Bearer ${api_token}`, + }, + mode: 'cors', + body: `{"template":"${template}"}`, }); - } else if (res.status == 400) { + t.hideToast(); + if (res.status == 200) { + toast({ + text: await res.text(), + color: 'var(--ctp-mocha-green)', + }); + } else if (res.status == 400) { + toast({ + text: (await res.json()).message, + color: 'var(--ctp-mocha-red)', + }); + } else if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + } else { + toast({ + text: await res.text(), + color: 'var(--ctp-mocha-red)', + }); + } + } catch (e) { + t.hideToast(); toast({ - text: (await res.json()).message, - color: 'var(--ctp-mocha-red)', - }); - } else if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); - } else { - toast({ - text: await res.text(), + text: 'Check CORS settings on HomeAssistant server.', color: 'var(--ctp-mocha-red)', }); + document.querySelector('#api_url').classList.add('invalid'); } }, '' @@ -602,45 +705,54 @@ require(['vs/editor/editor.main'], async () => { const t = toast({ text: 'Running action...', }); - const res = await fetch( - api_url + '/services/' + service[0] + '/' + service[1], - { - method: 'POST', - headers: { - Authorization: `Bearer ${api_token}`, - }, - mode: 'cors', - body: JSON.stringify(data), - } - ); - t.hideToast(); - if (res.status == 200) { - toast({ - text: 'Success', - color: 'var(--ctp-mocha-green)', - }); - } else if (res.status == 400) { - const text = await res.text(); - try { + try { + const res = await fetch( + api_url + '/services/' + service[0] + '/' + service[1], + { + method: 'POST', + headers: { + Authorization: `Bearer ${api_token}`, + }, + mode: 'cors', + body: JSON.stringify(data), + } + ); + t.hideToast(); + if (res.status == 200) { toast({ - text: JSON.parse(text).message, - color: 'var(--ctp-mocha-red)', + text: 'Success', + color: 'var(--ctp-mocha-green)', }); - } catch { + } else if (res.status == 400) { + const text = await res.text(); + try { + toast({ + text: JSON.parse(text).message, + color: 'var(--ctp-mocha-red)', + }); + } catch { + toast({ + text: text, + color: 'var(--ctp-mocha-red)', + }); + } + } else if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + } else { toast({ - text: text, + text: await res.text(), color: 'var(--ctp-mocha-red)', }); } - } else if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); - } else { + makeMarkers(); + } catch (e) { + t.hideToast(); toast({ - text: await res.text(), + text: 'Check CORS settings on HomeAssistant server.', color: 'var(--ctp-mocha-red)', }); + document.querySelector('#api_url').classList.add('invalid'); } - makeMarkers(); }, '' ); @@ -652,44 +764,56 @@ require(['vs/editor/editor.main'], async () => { const t = toast({ text: 'Toggling...', }); - const res = await fetch(api_url + '/services/' + entity[0] + '/toggle', { - method: 'POST', - headers: { - Authorization: `Bearer ${api_token}`, - }, - mode: 'cors', - body: JSON.stringify({ - entity_id: item.entity, - }), - }); - t.hideToast(); - if (res.status == 200) { - toast({ - text: 'Success', - color: 'var(--ctp-mocha-green)', - }); - } else if (res.status == 400) { - const text = await res.text(); - try { + try { + const res = await fetch( + api_url + '/services/' + entity[0] + '/toggle', + { + method: 'POST', + headers: { + Authorization: `Bearer ${api_token}`, + }, + mode: 'cors', + body: JSON.stringify({ + entity_id: item.entity, + }), + } + ); + t.hideToast(); + if (res.status == 200) { toast({ - text: JSON.parse(text).message, - color: 'var(--ctp-mocha-red)', + text: 'Success', + color: 'var(--ctp-mocha-green)', }); - } catch { + } else if (res.status == 400) { + const text = await res.text(); + try { + toast({ + text: JSON.parse(text).message, + color: 'var(--ctp-mocha-red)', + }); + } catch { + toast({ + text: text, + color: 'var(--ctp-mocha-red)', + }); + } + } else if (res.status == 401 || res.status == 403) { + document.querySelector('#api_token').classList.add('invalid'); + } else { toast({ - text: text, + text: await res.text(), color: 'var(--ctp-mocha-red)', }); } - } else if (res.status == 401 || res.status == 403) { - document.querySelector('#api_token').classList.add('invalid'); - } else { + makeMarkers(); + } catch (e) { + t.hideToast(); toast({ - text: await res.text(), + text: 'Check CORS settings on HomeAssistant server.', color: 'var(--ctp-mocha-red)', }); + document.querySelector('#api_url').classList.add('invalid'); } - makeMarkers(); }, '' ); @@ -854,6 +978,7 @@ require(['vs/editor/editor.main'], async () => { monaco.editor.setModelMarkers(model, 'template', markers); } catch {} } + window.makeMarkers = makeMarkers; makeMarkers(); model.onDidChangeContent(async function () {