'use strict'; import "./webui.css"; import { html, render } from 'lit-html'; import { map } from 'lit-html/directives/map.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { unsafeSVG } from 'lit-html/directives/unsafe-svg.js'; import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; import { connect } from "mqtt"; var client; const connectedSvg = ''; const disconnectedSvg = ''; const backButtonSvg = ''; const goToPage = (pageHash) => { if (typeof pageHash !== 'string') pageHash = location.hash || '#mainpage'; console.log('goToPage', pageHash); document.querySelectorAll(".page").forEach(element => { element.style.display = ('#'+element.id !== pageHash) ? 'none' : ''; }); } const transformMessage = (input, meta) => { if (typeof meta.transform !== 'object') return input; if (typeof meta.transform.set === 'string') { try { return new Function('input', meta.transform.set)(input); } catch (exception){ console.log(exception); return; } } return input; } const onMessage = (topic, message) => { try { message = JSON.parse(message.toString()); } catch(e) { } document.querySelectorAll('[data-mqtt-topic="' + topic + '"]').forEach((element) => { let value = null; if (typeof element.dataset.transform !== 'undefined') { try { value = new Function('topic', 'message', element.dataset.transform)(topic, message); } catch (exception){ console.log(exception); return; } } else { value = message; } if (value === null) value = message; switch (element.dataset.type) { case 'html': element.innerHTML = value; break; case 'text': if (typeof value === 'object') return; element.textContent = value; break; case 'switch': if (typeof value !== 'boolean') return; element.checked = value; break; case 'slider': if (isNaN(value)) return; element.value = value; element.dataset.lastMqttValue = value; element.style.setProperty('--c', 0); break; case 'select': if (typeof value === 'object') return; element.value = value; element.dataset.lastMqttValue = value; element.parentNode.getElementsByTagName('div')[0].classList.remove('loader'); break; case 'button': if (typeof value === 'object') return; element.classList.remove('working'); if (element.dataset.mqttValue === value) { element.classList.add('active'); } else { element.classList.remove('active'); } break; case 'number': if (isNaN(value)) return; element.value = value; element.style.width = (element.value.length + 2) + "ch"; element.classList.remove('working'); break; } }); }; const getConfig = async () => { const response = await fetch('/config.json'); if (!response.ok) { if (!localStorage.getItem('config')) return { pages: [{ pageid: "mainpage", pagetitle: "No Config!", sections: [] }] }; return JSON.parse(localStorage.getItem('config')); } const config = await response.json(); localStorage.setItem('config', JSON.stringify(config)); return config; }; const renderItem = (itemData) => { if (typeof itemData.topic === 'string') { const temporary = itemData.topic; itemData.topic = {}; itemData.topic.get = temporary; itemData.topic.set = null; } if (typeof itemData.topic === 'undefined') itemData.topic = {} let transform = null; if (typeof itemData.transform !== 'undefined') { if (typeof itemData.transform === 'string') transform = itemData.transform; if (typeof itemData.transform.get === 'string') transform = itemData.transform.get; }; if (itemData.type !== 'html') { let element = html``; switch (itemData.type) { case 'text': if (typeof itemData.topic === 'undefined') break; element = html``; break; case 'switch': const switchChangeHandler = (event) => { if (!client.connected) return false; if (itemData.topic.set === null) return false; client.publish(itemData.topic.set, transformMessage(event.target.checked, itemData), itemData.mqtt); return false; } element = html` `; break; case 'slider': const sliderChangeHandler = (event) => { if (!client.connected) return false; if (itemData.topic.set === null) return false; client.publish(itemData.topic.set, transformMessage(event.target.value, itemData), itemData.mqtt); } const sliderInputHandler = (event) => { if (!client.connected) return false; if (itemData.topic.set === null) return false; const element = event.target; const value = (element.dataset.lastMqttValue - element.value) / (element.max - element.min) * (element.getBoundingClientRect().width - 20); event.target.style.setProperty('--c', value + 'px'); } itemData.sliderMinValue ??= 0; itemData.sliderMaxValue ??= 1; itemData.sliderStepValue ??= 'any'; element = html``; break; case 'select': const selectChangeHandler = (event) => { if (!client.connected) return false; if (itemData.topic.set === null) return false const element = event.target; element.parentNode.getElementsByTagName('div')[0].classList.add('loader'); client.publish(itemData.topic.set, transformMessage(element.value, itemData), itemData.mqtt); // Reset to last known state element.value = element.dataset.lastMqttValue } element = html`
`; break; case 'button': const buttonClickHandler = (event) => { if (!client.connected) return false; if (itemData.topic.set === null) return false; event.target.classList.add('working'); client.publish(itemData.topic.set, transformMessage(event.target.dataset.mqttValue, itemData), itemData.mqtt); return false; } const buttonElement = (button) => html``; element = html`
${map(itemData.buttons, buttonElement)}
`; break; case 'number': const keydownHandler = (event) => { const element = event.target; element.style.width = (element.value.length + 2) + "ch"; if (event.code === 'Backspace') return true; if (event.code === 'Delete') return true; if (!(/^([.,0-9]+)$/.test(event.key))) { event.preventDefault(); return false; } } const numberHandlder = (event) => { if (!client.connected) return false; if (itemData.topic.set === null) return false; const input = event.target.parentNode.querySelector('input'); let inputValue = Number(input.value.replace(',', '.')); itemData.numberMinValue ??= 1; itemData.numberMaxValue ??= 254; itemData.numberIncrement ??= 1; itemData.numberDecrement ??= 1; if (event.target.textContent === '-') { if (inputValue === Number(itemData.numberMinValue)) return false; inputValue = inputValue - itemData.numberDecrement; } if (event.target.textContent === '+') { if (inputValue === Number(itemData.numberMaxValue)) return false; inputValue = inputValue + itemData.numberIncrement; } const transformedValue = transformMessage(inputValue, itemData); client.publish(itemData.topic.set, transformedValue, itemData.mqtt); input.classList.add('working'); } element = html`
`; break; } const clickHandler = (itemData.link) ? (event) => { window.location = itemData.link; } : null; const cursorStyle = (itemData.link) ? 'cursor: pointer;' : null; return html`
${itemData.title}
${element}
`; } else { return html`
${unsafeHTML(itemData.html)}
`; } } const renderSection = (sectionConfig) => { const title = (sectionConfig.title) ? html`
${sectionConfig.title}
` : html`` return html`
${title} ${map(sectionConfig.items, (itemData) => renderItem(itemData))}
`; } const renderPage = (pageConfig) => { const backButton = (pageConfig.id !== 'mainpage') ? html`` : html ``; const image = (typeof pageConfig.icon === 'string') ? html`` : html``; return html`
${map(pageConfig.sections, (sectionConfig) => renderSection(sectionConfig))}
`; } const renderPages = (config) => html`${map(config.pages, (pageConfig) => renderPage(pageConfig))}`; const scrollHandler = (event) => { let elements = [...document.getElementsByTagName('nav')]; if (window.scrollY > 0) { elements.forEach((element) => element.classList.add('scroll')); } else { elements.forEach((element) => element.classList.remove('scroll')); } } window.addEventListener('hashchange', goToPage); window.addEventListener("scroll", scrollHandler); window.addEventListener('DOMContentLoaded', async (event) => { const config = await getConfig(); const topics = new Set(); const mqttUrl = 'ws' + (location.protocol === 'https:' ? 's' : '') + '://' + location.hostname + ((location.port === '') ? '' : ':' + location.port) + '/mqtt'; const mqttOptions = { clientId: localStorage.getItem('instanceId') ?? 'mqtt-webui_' + Math.random().toString(36).slice(2) }; localStorage.setItem('instanceId', mqttOptions.clientId); if (typeof config.appName === 'string') { document.getElementsByTagName('title')[0].textContent = config.appName; document.querySelector('meta[name="apple-mobile-web-app-title"]').content = config.appName; } if (typeof config.extraCSS === 'string') { const link = document.createElement('link'); link.href = config.extraCSS; link.type = 'text/css'; link.rel = 'stylesheet'; document.getElementsByTagName('head')[0].appendChild(link); } // create UI render(renderPages(config), document.body); document.getElementById("loading").style.display = 'none'; goToPage(); if (!config.disableAuth) { mqttOptions.username = localStorage.getItem('username') ?? prompt("username:"); mqttOptions.password = localStorage.getItem('password') ?? prompt("password:"); localStorage.setItem('username', mqttOptions.username); localStorage.setItem('password', mqttOptions.password); } if (navigator.serviceWorker) { navigator.serviceWorker.register('sw.js', { scope: './' }).then(function(reg) { console.log('serviceWorker registration succeeded.'); }); } config.pages.forEach((page) => { page.sections.forEach((section) => { section.items.forEach((item) => { if (typeof item.topic === 'undefined') return; if (typeof item.topic === 'string') topics.add(item.topic); if (typeof item.topic.get === 'string') topics.add(item.topic.get); }) }) }) // MQTT console.log('MQTT conencting to', mqttUrl); client = connect(mqttUrl, mqttOptions); client.on('connect', () => { console.log('Connected to mqtt-server!') document.querySelectorAll('.connectionStatus').forEach((element) => element.innerHTML = connectedSvg); }); client.on('reconnect', () => console.log('Trying reconnect to mqtt-server!')); client.on('offline', () => { console.log('Disconnected from mqtt-server!') document.querySelectorAll('.connectionStatus').forEach((element) => element.innerHTML = disconnectedSvg); }); client.on('error', (error) => { if (error.message === 'Connection refused: Not authorized') { alert('Authentication failed!'); if (!config.disableAuth) { localStorage.setItem('username', prompt('username:') ?? localStorage.getItem('username')); localStorage.setItem('password', prompt('password:') ?? localStorage.getItem('password')); } } location.reload() }); client.on('message', onMessage); client.subscribe([...topics]) });