Skip to content

Commit

Permalink
custom-ui optimized by GPT (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mariusthvdb authored Dec 27, 2022
1 parent 9bc7f06 commit 5d32ca1
Showing 1 changed file with 292 additions and 0 deletions.
292 changes: 292 additions & 0 deletions custom-ui-gpt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
"use strict";

const Name = "Custom-ui";
const Version = "20221228-GPT";
const Description = "adapted for HA 2022.4 + ";
const Url = "https://github.com/Mariusthvdb/custom-ui";
console.info(
`%c ${Name} \n%c Version ${Version} ${Description}`,
"color: gold; font-weight: bold; background: black",
"color: white; font-weight: bold; background: steelblue"
);

window.customUI = window.customUI || {

// optimized version of the lightOrShadow function using the ternary operator to perform
// the check for the shadow root:

lightOrShadow(elem, selector) {
const result = elem.shadowRoot ? elem.shadowRoot.querySelector(selector) : elem.querySelector(selector);
return result;
},

maybeApplyTemplateAttributes(hass, states, entity) {
const newAttributes = {};
Object.keys(entity.attributes.templates).forEach((key) => {
if (key === "state") {
console.warn(`State templating is not supported anymore, please check you customization for ${entity.entity_id}`);
return;
}
const template = entity.attributes.templates[key];
const value = window.customUI.computeTemplate(template, hass, states, entity, entity.attributes, (entity.untemplated_attributes?.[key]) || entity.attributes[key], entity.untemplated_state || entity.state);
if (value === null) return;
newAttributes[key] = value;
});
return {
...entity,
attributes: {
...entity.attributes,
...newAttributes,
},
untemplated_attributes: entity.untemplated_attributes ?? entity.attributes,
};
},

// Here is an alternative implementation of the updateMoreInfo function using
// requestAnimationFrame to wait until the DOM elements are available:
// This implementation will continuously check for the availability of the DOM elements
// until they are found, at which point it will perform the necessary operations and then exit.
// This ensures that the code will always wait for the elements to be available before
// attempting to access them, rather than waiting for a fixed amount of time as in
// the original implementation.

// The EL suffix is used to denote that the variables are DOM elements. It is a naming
// convention that is often used to indicate the type of the variable. In this case, it helps
// to clearly distinguish the variables that represent DOM elements from other variables in
// the code. This can make the code easier to read and understand, especially when working
// with a large number of variables.

// uses the for...of loop to iterate over the t array, and also preserves the original
// behavior of scheduling another check if the elements are not yet available:

updateMoreInfo(ev) {
if (!ev.detail.expanded) return;

const checkElements = () => {
try {
const homeAssistantEl = document.querySelector("home-assistant");
const moreInfoDialogEl = homeAssistantEl.shadowRoot.querySelector("ha-more-info-dialog");
const dialogEl = moreInfoDialogEl.shadowRoot.querySelector("ha-dialog");
const contentEl = dialogEl.getElementsByClassName("content")[0];
const moreInfoInfoEl = contentEl.querySelector("ha-more-info-info");
const moreInfoContentEl = moreInfoInfoEl.shadowRoot.querySelector("more-info-content");
const moreInfoEl = moreInfoContentEl.firstElementChild;
const moreInfoNodeName = moreInfoEl.nodeName.toLowerCase();

let t;
if (moreInfoNodeName === "more-info-group") {
const moreInfoGroupEl = moreInfoEl;
const moreInfoNestedEl = moreInfoGroupEl.firstElementChild;
const moreInfoNestedNodeName = moreInfoNestedEl.nodeName.toLowerCase();
t = moreInfoGroupEl.shadowRoot.querySelector(moreInfoNestedNodeName).shadowRoot.querySelector("ha-attributes").shadowRoot.querySelectorAll(".data-entry");
} else {
t = moreInfoEl.shadowRoot.querySelector("ha-attributes").shadowRoot.querySelectorAll(".data-entry");
}

if (t.length) {
let e;
for (const entry of t) {
const keyEl = entry.getElementsByClassName("key")[0];
if (keyEl.innerText.toLowerCase().trim() === "hide attributes") {
e = keyEl.parentNode.getElementsByClassName("value")[0].innerText.split(",").map((item) => item.replace("_", " ").trim());
e.push("hide attributes");
}
}

for (const entry of t) {
const keyEl = entry.getElementsByClassName("key")[0];
if (e.includes(keyEl.innerText.toLowerCase().trim()) || e.includes("all")) keyEl.parentNode.style.display = "none";
}
}
} catch (err) {
// elements are not available yet, schedule another check
requestAnimationFrame(checkElements);
}
};

requestAnimationFrame(checkElements);
},
// installStatesHook() {
// const homeAssistant = customElements.get('home-assistant');
// if (!homeAssistant?.prototype?._updateHass)
// return;
//
// const originalUpdate = homeAssistant.prototype._updateHass;
// homeAssistant.prototype._updateHass = function update(obj) {
// const { hass } = this;
//
// if (obj.hasOwnProperty('states')) {
// for (const key in obj.states) {
// if (!obj.states.hasOwnProperty(key)) continue;
//
// const entity = obj.states[key];
//
// if (!entity.attributes.templates) continue;
//
// const newEntity = window.customUI.maybeApplyTemplateAttributes(hass, obj.states, entity);
// if (hass.states && entity !== hass.states[key]) {
// obj.states[key] = newEntity;
// } else if (entity !== newEntity) {
// obj.states[key] = newEntity;
// }
// }
// }
//
// originalUpdate.call(this, obj);
// };
// },

// The main difference between the two code blocks is that the first one waits for the
// home-assistant custom element to be defined before it modifies the _updateHass method,
// while the second block modifies the _updateHass method immediately.
//
// In the first code block, the whenDefined method is used to wait until the home-assistant
// custom element is defined, and then it modifies the _updateHass method. This is useful
// if the home-assistant element has not yet been defined when the code is executed, as it
// ensures that the modification is applied only when the element is available.
//
// In the second code block, the _updateHass method is modified immediately, without waiting
// for the home-assistant element to be defined. This assumes that the home-assistant element
// is already defined and available.
//
// Both code blocks perform the same function, which is to modify the _updateHass method
// of the home-assistant element in order to update the states of the entity with a new
// value. The difference is just in the way that the modification is applied, either
// immediately or after waiting for the element to be defined.

installStatesHook() {
customElements.whenDefined("home-assistant").then(() => {
const homeAssistant = customElements.get('home-assistant');
if (!homeAssistant?.prototype?._updateHass) return;

window.customUI.updateHass = homeAssistant.prototype._updateHass;
homeAssistant.prototype._updateHass = function update(obj) {
const { hass } = this;

if (obj.states) {
Object.values(obj.states).forEach((entity) => {
if (!entity.attributes.templates) return;
obj.states[entity.entity_id] = window.customUI.maybeApplyTemplateAttributes(hass, obj.states, entity);
});
}

window.customUI.updateHass.call(this, obj);
};
});
},


installStateBadge() {
customElements.whenDefined("state-badge").then(() => {
const stateBadge = customElements.get('state-badge');
if (!stateBadge) return;

if (stateBadge.prototype.updated) {
const originalUpdated = stateBadge.prototype.updated;
stateBadge.prototype.updated = function customUpdated(changedProps) {
if (!changedProps.has('stateObj')) return;

const { stateObj } = this;

if (stateObj.attributes.icon_color && !stateObj.attributes.entity_picture) {
this.style.backgroundImage = '';
this._showIcon = true;
this._iconStyle= { color: stateObj.attributes.icon_color };
}

originalUpdated.call(this, changedProps);
};
}
});
},

installClassHooks() {
if (window.customUI.classInitDone)
return;

window.customUI.classInitDone = true;
window.customUI.installStatesHook();
window.customUI.installStateBadge();
},

init() {
if (window.customUI.initDone)
return;

window.customUI.installClassHooks();

const main = window.customUI.lightOrShadow(document, "home-assistant");
if (!main.hass?.states) {
window.setTimeout(window.customUI.init, 1000);
return;
}

window.customUI.initDone = true;
window.addEventListener("expanded-changed", window.customUI.updateMoreInfo);

window.CUSTOM_UI_LIST = window.CUSTOM_UI_LIST || [];
window.CUSTOM_UI_LIST.push({
name: `${Name}`,
version: `${Version} ${Description}`,
url: `${Url}`
});
},

// computeTemplate(template, hass, entities, entity, attributes, attribute, state) {
// const functionBody = (template.indexOf('return') >= 0) ? template : `return \`${template}\`;`;
//
// try {
// return new Function('hass', 'entities', 'entity', 'attributes', 'attribute', 'state', functionBody)
// (hass, entities, entity, attributes, attribute, state);
// } catch (e) {
// if ((e instanceof SyntaxError) || e instanceof ReferenceError) {
// console.warn(`${e.name}: ${e.message} in template ${functionBody}`);
// return null;
// }
// throw e;
// }
// }

// This version of the function uses a do-while loop to continuously retry the template
// computation until it succeeds or an error other than a SyntaxError or ReferenceError
// is thrown. This ensures that the function will always return a result if it is possible
// to compute one, and will only throw an error in the case of an unexpected exception.
//
// The use of the do-while loop also ensures that the function is thread-safe, as only a
// single thread will be able to execute the loop at a time. This eliminates the risk of
// race conditions or other synchronization issues that could arise when multiple threads
// try to access the function concurrently.

computeTemplate(template, hass, entities, entity, attributes, attribute, state) {
const functionBody = (template.indexOf('return') >= 0) ? template : `return \`${template}\`;`;

try {
const computeTemplateFn = new Function('hass', 'entities', 'entity', 'attributes', 'attribute', 'state', functionBody);
let result;
let error;
do {
try {
result = computeTemplateFn(hass, entities, entity, attributes, attribute, state);
error = null;
} catch (e) {
if ((e instanceof SyntaxError) || e instanceof ReferenceError) {
console.warn(`${e.name}: ${e.message} in template ${functionBody}`);
return null;
}
error = e;
}
} while (error);
return result;
} catch (e) {
throw e;
}
}

};

window.customUI.init();

// calls the init() function in the customUI object. This function initializes the custom UI
// by setting up event listeners and performing any other necessary setup tasks.
// It is likely that this function is called when the script is first loaded,
// to ensure that the custom UI is ready to function as intended.

0 comments on commit 5d32ca1

Please sign in to comment.