Skip to content

Instantly share code, notes, and snippets.

@jag-k
Last active January 13, 2024 20:25
Show Gist options
  • Save jag-k/87f99013b91d8314cf0c1f056e003011 to your computer and use it in GitHub Desktop.
Save jag-k/87f99013b91d8314cf0c1f056e003011 to your computer and use it in GitHub Desktop.
HASS Persons bundle
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: users;
// Config
// You can change the config here
const WIDGET_TITLE = 'Persons at home';
// =========================================
// PLEASE, DON'T CHANGE ANYTHING BELOW THIS COMMENT,
// IF YOU DON'T KNOW WHAT YOU ARE DOING
// =========================================
let dm = config.widgetFamily; // Using display mode medium by default
function displayMode(setMode) {
if (setMode) {
dm = setMode;
}
return (dm || config.widgetFamily);
}
const FULL_CARD = () => displayMode() !== "small";
// =============================
// Widget JSX Element processing
// =============================
function processJSXColor(color) {
return typeof color === "string"
? new Color(color, 1)
: typeof color === "number"
? new Color("#" + color.toString(16).padStart(6, "0"), 1)
: color;
}
function processPaddingProps(widget, props, defaultPadding) {
widget.useDefaultPadding();
const paddings = defaultPadding || [0, 0, 0, 0];
if (props["p-all"] !== undefined) {
paddings[0] = props["p-all"];
paddings[1] = props["p-all"];
paddings[2] = props["p-all"];
paddings[3] = props["p-all"];
}
if (props["p-y"] !== undefined) {
paddings[0] = props["p-y"];
paddings[2] = props["p-y"];
}
if (props["p-x"] !== undefined) {
paddings[1] = props["p-x"];
paddings[3] = props["p-x"];
}
if (props["p-top"] !== undefined) {
paddings[0] = props["p-top"];
}
if (props["p-right"] !== undefined) {
paddings[1] = props["p-right"];
}
if (props["p-bottom"] !== undefined) {
paddings[2] = props["p-bottom"];
}
if (props["p-left"] !== undefined) {
paddings[3] = props["p-left"];
}
widget.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
}
function processTextProps(text, props) {
switch (props.align) {
case "left":
text.leftAlignText();
break;
case "right":
text.rightAlignText();
break;
case "center":
text.centerAlignText();
break;
}
if (props.font) {
text.font = props.font;
}
if (props.color) {
text.textColor = processJSXColor(props.color);
}
if (props.lineLimit) {
text.lineLimit = props.lineLimit;
}
if (props.minimumScaleFactor) {
text.minimumScaleFactor = props.minimumScaleFactor;
}
if (props.opacity) {
text.textOpacity = props.opacity;
}
if (props.shadowColor) {
text.shadowColor = processJSXColor(props.shadowColor);
}
if (props.shadowOffset) {
text.shadowOffset = props.shadowOffset;
}
if (props.shadowRadius) {
text.shadowRadius = props.shadowRadius;
}
if (props.url) {
text.url = props.url;
}
}
function processStackProps(stack, props) {
switch (props.align) {
case "top":
stack.topAlignContent();
break;
case "bottom":
stack.bottomAlignContent();
break;
case "center":
stack.centerAlignContent();
break;
}
if (props.backgroundColor) {
stack.backgroundColor = processJSXColor(props.backgroundColor);
}
if (props.backgroundGradient) {
stack.backgroundGradient = props.backgroundGradient;
}
if (props.backgroundImage) {
stack.backgroundImage = props.backgroundImage;
}
if (props.borderColor) {
stack.borderColor = processJSXColor(props.borderColor);
}
if (props.borderWidth) {
stack.borderWidth = props.borderWidth;
}
if (props.cornerRadius) {
stack.cornerRadius = props.cornerRadius;
}
if (props.size) {
stack.size = props.size;
}
if (props.spacing) {
stack.spacing = props.spacing;
}
if (props.url) {
stack.url = props.url;
}
processPaddingProps(stack, props, [0, 0, 0, 0]);
}
function processImageProps(image, props) {
switch (props.align) {
case "left":
image.leftAlignImage();
break;
case "right":
image.rightAlignImage();
break;
case "center":
image.centerAlignImage();
break;
}
if (props.size) {
image.imageSize = props.size;
}
if (props.borderColor) {
image.borderColor = processJSXColor(props.borderColor);
}
if (props.borderWidth) {
image.borderWidth = props.borderWidth;
}
if (props.containerRelativeShape) {
image.containerRelativeShape = props.containerRelativeShape;
}
switch (props.contentMode) {
case "filling":
image.applyFillingContentMode();
break;
case "fitting":
image.applyFittingContentMode();
break;
}
if (props.cornerRadius) {
image.cornerRadius = props.cornerRadius;
}
if (props.opacity) {
image.imageOpacity = props.opacity;
}
if (props.resizable) {
image.resizable = props.resizable;
}
if (props.tintColor) {
image.tintColor = processJSXColor(props.tintColor);
}
if (props.url) {
image.url = props.url;
}
}
function processWidgetProps(widget, props) {
if (props.backgroundColor) {
widget.backgroundColor = processJSXColor(props.backgroundColor);
}
if (props.backgroundGradient) {
widget.backgroundGradient = props.backgroundGradient;
}
if (props.backgroundImage) {
widget.backgroundImage = props.backgroundImage;
}
if (props.refreshAfterDate) {
widget.refreshAfterDate = props.refreshAfterDate;
}
if (props.spacing !== undefined) {
widget.spacing = props.spacing;
}
if (props.url) {
widget.url = props.url;
}
processPaddingProps(widget, props, [15, 15, 15, 15]);
}
function processContainerChildren(widget, children) {
for (const child of children) {
if (child === null || child === undefined || child === false ||
typeof child === "function") {
continue;
}
else if (typeof child === "string" || typeof child === "number" ||
typeof child === "bigint" || typeof child === "symbol" || child === true) {
widget.addText(String(child));
}
else if (typeof child === "object") {
if (child instanceof Array) {
processContainerChildren(widget, child);
}
else if (child instanceof Date) {
widget.addDate(child);
}
else if (child instanceof Image) {
widget.addImage(child);
}
else if ("type" in child) {
switch (child.type) {
case "text": {
const text = widget.addText(child.text);
processTextProps(text, child.props || {});
break;
}
case "date": {
const init = child.props.date;
const date = widget.addDate(init instanceof Date ? init : new Date(init));
switch (child.props.style) {
case "date":
date.applyDateStyle();
break;
case "offset":
date.applyOffsetStyle();
break;
case "time":
date.applyTimeStyle();
break;
case "relative":
date.applyRelativeStyle();
break;
case "timer":
date.applyTimerStyle();
break;
}
processTextProps(date, child.props || {});
break;
}
case "stack": {
const stack = widget.addStack();
switch (child?.props?.layout) {
case "vertical":
stack.layoutVertically();
break;
case "horizontal":
stack.layoutHorizontally();
break;
}
processStackProps(stack, child.props || {});
processContainerChildren(stack, child.children);
break;
}
case "spacer": {
const spacer = widget.addSpacer();
if (child?.props?.size) {
spacer.length = child.props.length;
}
break;
}
case "image": {
let init;
const { data } = child.props || {};
if (typeof data === "string") {
init = Image.fromFile(child.props.fileURL);
}
else if (data.size) {
init = data;
}
else {
init = Image.fromData(data);
}
const image = widget.addImage(init);
processImageProps(image, child.props || {});
break;
}
}
}
}
}
}
function widgetCreateElement(element, props, ...children) {
if (typeof element === "string") {
// intrinsic element
switch (element) {
case "widget":
const widget = new ListWidget();
processContainerChildren(widget, children);
processWidgetProps(widget, props || {});
return widget;
case "text":
let text = "";
for (let child of children) {
if (typeof child === "string" ||
typeof child === "number" ||
typeof child === "bigint") {
text += child;
}
}
return { type: element, props, text };
case "stack":
return { type: element, props, children };
case "spacer":
return { type: element, props };
case "image":
return { type: element, props };
case "date":
return { type: element, props };
}
}
}
// =============================
// Alert JSX Element processing
// =============================
function processAlertProps(alert, props) {
if (props.title) {
alert.title = props.title;
}
if (props.message) {
alert.message = props.message;
}
}
function processActionProps(alert, props, children) {
const text = props.text || (children instanceof Array ? children.join(" ") : children) || "";
switch (props.type || "action") {
case "action": {
alert.addAction(text);
break;
}
case "cancel": {
alert.addCancelAction(text);
break;
}
case "destructive": {
alert.addDestructiveAction(text);
break;
}
}
}
function processTextFieldProps(alert, props) {
(props.secure ? alert.addSecureTextField : alert.addTextField)(props.placeholder || "", props.text || "");
}
function processAlertChildren(alert, children) {
for (const child of children) {
if (typeof child === "string") {
return child;
}
if (typeof child === "object") {
if (child instanceof Array) {
processAlertChildren(alert, child);
}
else if ("type" in child) {
switch (child.type) {
case "action": {
processActionProps(alert, child.props || {}, child.children || "");
break;
}
case "text-field": {
processTextFieldProps(alert, child.props || {});
break;
}
}
}
}
}
}
function alertCreateElement(element, props, ...children) {
if (typeof element === "string") {
// intrinsic element
switch (element) {
case "alert":
const alert = new Alert();
processAlertChildren(alert, children);
processAlertProps(alert, props || {});
return alert;
case "text-field":
return { type: element, props };
case "action":
return { type: element, props, children };
}
}
}
const createElements = [widgetCreateElement, alertCreateElement];
class ScriptableJSX {
static createElement(element, props, ...children) {
for (const creator of createElements) {
const res = creator(element, props, ...children);
if (res) {
return res;
}
}
}
}
// =========================================
// Constants
// =========================================
const HASS_URL = "HASS_URL";
const HASS_API_TOKEN = "HASS_TOKEN";
// =========================================
// Colors
// =========================================
// language=CSS prefix=*{color:# suffix=;}
const BackgroundColor = Color.dynamic(new Color('ccc', 1), new Color('1c1c1c', 1));
// language=CSS prefix=*{color:# suffix=;}
const SecondBackgroundColor = Color.dynamic(new Color('000', .1), new Color('fff', .1));
// language=CSS prefix=*{color:# suffix=;}
const SecondActiveBackgroundColor = Color.dynamic(new Color('000', .2), new Color('fff', .2));
// language=CSS prefix=*{color:# suffix=;}
const TextColor = Color.dynamic(new Color('000', 1), new Color('fff', 1));
// language=CSS prefix=*{color:# suffix=;}
const TransparentColor = new Color('000', 0);
function baseUrl(baseUrl) {
return baseUrl.replace(/\/?$/, '');
}
function createRequest(url, token) {
const req = new Request(url);
req.headers = { 'Authorization': `Bearer ${token || Keychain.get(HASS_API_TOKEN)}` };
return req;
}
async function getAPIStatus(url, token) {
const apiCheckUrl = (url ? baseUrl(url) : Keychain.get(HASS_URL)) + '/api/';
const req = createRequest(apiCheckUrl, token);
console.log(`Checking ${apiCheckUrl}`);
const d = await req.loadString();
console.log(`d: ${d}`);
const data = JSON.stringify(JSON.parse(d));
console.log(`data: ${data}`);
return data === JSON.stringify({ "message": "API running." });
}
async function getAllStates() {
return await createRequest(Keychain.get(HASS_URL) + '/api/states').loadJSON();
}
async function getPersonStates() {
const states = await getAllStates();
return states.filter(state => state.entity_id.startsWith('person.'));
}
async function getPicture(person) {
return await createRequest(Keychain.get(HASS_URL) + person.attributes.entity_picture).loadImage();
}
function createPersonURL(person) {
return `${Keychain.get(HASS_URL)}/history?entity_id=${person.entity_id}`;
}
async function join(array, callback, separator) {
if (!separator) {
separator = ScriptableJSX.createElement("spacer", null);
}
const result = [];
const length = array.length;
for (let i = 0; i < length; i++) {
const value = array[i];
const out = await callback(value);
if (!out) {
break;
}
result.push(out);
if (i !== length - 1)
result.push(separator);
}
return result;
}
function getKeychain(key) {
if (Keychain.contains(key)) {
return Keychain.get(key);
}
}
async function login(url, token) {
let argsPassed = true;
if (!(url && token)) {
if (!(Keychain.contains(HASS_URL) && Keychain.contains(HASS_API_TOKEN))) {
return false;
}
url = getKeychain(HASS_URL);
token = getKeychain(HASS_API_TOKEN);
argsPassed = false;
}
if (url && token) {
const resp = await getAPIStatus(url, token);
if (!resp) {
return false;
}
if (config.runsInApp && argsPassed) {
const a = (ScriptableJSX.createElement("alert", { title: "Login Successful!", message: "Now you can use the widget!" },
ScriptableJSX.createElement("action", { type: "cancel" }, "Close")));
await a.present();
}
console.log(`Success login!`);
console.log(`${url}, ${token}`);
Keychain.set(HASS_URL, baseUrl(url));
Keychain.set(HASS_API_TOKEN, token);
return true;
}
return false;
}
async function loginMenu() {
const a = (ScriptableJSX.createElement("alert", { title: "Login in HomeAssistant" },
ScriptableJSX.createElement("text-field", { placeholder: 'Full URL to HomeAssistant URL', text: getKeychain(HASS_URL) }),
ScriptableJSX.createElement("text-field", { placeholder: 'HomeAssistant Token', text: getKeychain(HASS_URL), secure: true }),
ScriptableJSX.createElement("action", { type: "cancel" }, "Cancel"),
ScriptableJSX.createElement("action", null, "Login")));
let res = await a.present();
console.log(`${res}, ${a.textFieldValue(0)}, ${a.textFieldValue(1)}`);
const [url, token] = [a.textFieldValue(0), a.textFieldValue(1)];
return await login(url, token);
}
// =========================================
// Widgets
// =========================================
async function loginWidget() {
return (ScriptableJSX.createElement("widget", { backgroundColor: BackgroundColor },
ScriptableJSX.createElement("text", null, "Before using Widget, please login!")));
}
async function personsWidget() {
const persons = await getPersonStates();
let personGrid = [];
switch (displayMode()) {
case "small":
personGrid = [
[persons[0], persons[1]],
[persons[2], persons[3]],
];
break;
case "medium":
personGrid = [
[persons[0], persons[1], persons[2], persons[3]],
];
break;
case "large":
case "extraLarge":
personGrid = [
[persons[0], persons[1], persons[2], persons[3]],
[persons[4], persons[5], persons[6], persons[7]],
[persons[8], persons[9], persons[10], persons[11]],
[persons[12], persons[13], persons[14], persons[15]],
];
break;
}
const widgetTitleText = {
font: Font.title3(),
color: TextColor,
};
const widgetTitle = FULL_CARD() ? [
ScriptableJSX.createElement("stack", { layout: "horizontal" },
ScriptableJSX.createElement("text", { ...widgetTitleText }, WIDGET_TITLE),
([
ScriptableJSX.createElement("spacer", null),
ScriptableJSX.createElement("text", { ...widgetTitleText },
persons.filter(p => p.state === 'home').length,
"/",
persons.length)
])),
ScriptableJSX.createElement("spacer", null)
] : [];
const jsxPersonsGrid = await join(personGrid, async (row) => (ScriptableJSX.createElement("stack", { layout: "horizontal" }, await join(row, personCard))));
return (ScriptableJSX.createElement("widget", { backgroundColor: BackgroundColor },
widgetTitle,
ScriptableJSX.createElement("stack", { layout: "vertical" }, jsxPersonsGrid)));
}
async function personCard(person) {
if (!person)
return ScriptableJSX.createElement("stack", null);
const at_home = person.state === 'home';
const opacity = at_home ? 1 : 0.5;
const image = await getPicture(person);
const fontSize = 14;
const props = (FULL_CARD() ?
{
spacing: 5,
'p-all': 7,
cornerRadius: 15,
backgroundColor: at_home ? SecondActiveBackgroundColor : SecondBackgroundColor
} : {});
const friendlyName = (ScriptableJSX.createElement("stack", { layout: "horizontal" },
ScriptableJSX.createElement("spacer", null),
ScriptableJSX.createElement("text", { color: TextColor, font: at_home ? Font.mediumSystemFont(fontSize) : Font.regularSystemFont(fontSize), opacity: opacity }, person.attributes.friendly_name),
ScriptableJSX.createElement("spacer", null)));
return (ScriptableJSX.createElement("stack", { layout: "vertical", ...props, url: createPersonURL(person) },
ScriptableJSX.createElement("image", { data: image, align: "center", opacity: opacity, cornerRadius: image.size.width / 2, borderWidth: 5, borderColor: at_home ? TextColor : TransparentColor, resizable: true }),
FULL_CARD() && friendlyName));
}
let loggedIn = await login();
const widget = loggedIn ? personsWidget : loginWidget;
if (config.runsInWidget) {
Script.setWidget(await widget());
}
else if (config.runsInApp) {
if (loggedIn) {
// Showing all widgets
displayMode('small');
const widgetSmall = await widget();
await widgetSmall.presentSmall();
displayMode('medium');
const widgetMedium = await widget();
await widgetMedium.presentMedium();
displayMode('large');
const widgetLarge = await widget();
await widgetLarge.presentLarge();
displayMode('extraLarge');
const widgetExtraLarge = await widget();
await widgetExtraLarge.presentExtraLarge();
}
else {
await loginMenu();
}
}
Script.complete();
{"always_run_in_app":false,"icon":{"color":"blue","glyph":"users"},"name":"HASS Persons","share_sheet_inputs":[],"default":{"always_run_in_app":false,"icon":{"color":"blue","glyph":"users"},"name":"HASS Persons","share_sheet_inputs":[]},"script":"// Config\n// You can change the config here\nconst WIDGET_TITLE = 'Persons at home';\n// =========================================\n// PLEASE, DON'T CHANGE ANYTHING BELOW THIS COMMENT,\n// IF YOU DON'T KNOW WHAT YOU ARE DOING\n// =========================================\nlet dm = config.widgetFamily; // Using display mode medium by default\nfunction displayMode(setMode) {\n if (setMode) {\n dm = setMode;\n }\n return (dm || config.widgetFamily);\n}\nconst FULL_CARD = () => displayMode() !== \"small\";\n\n// =============================\n// Widget JSX Element processing\n// =============================\nfunction processJSXColor(color) {\n return typeof color === \"string\"\n ? new Color(color, 1)\n : typeof color === \"number\"\n ? new Color(\"#\" + color.toString(16).padStart(6, \"0\"), 1)\n : color;\n}\nfunction processPaddingProps(widget, props, defaultPadding) {\n widget.useDefaultPadding();\n const paddings = defaultPadding || [0, 0, 0, 0];\n if (props[\"p-all\"] !== undefined) {\n paddings[0] = props[\"p-all\"];\n paddings[1] = props[\"p-all\"];\n paddings[2] = props[\"p-all\"];\n paddings[3] = props[\"p-all\"];\n }\n if (props[\"p-y\"] !== undefined) {\n paddings[0] = props[\"p-y\"];\n paddings[2] = props[\"p-y\"];\n }\n if (props[\"p-x\"] !== undefined) {\n paddings[1] = props[\"p-x\"];\n paddings[3] = props[\"p-x\"];\n }\n if (props[\"p-top\"] !== undefined) {\n paddings[0] = props[\"p-top\"];\n }\n if (props[\"p-right\"] !== undefined) {\n paddings[1] = props[\"p-right\"];\n }\n if (props[\"p-bottom\"] !== undefined) {\n paddings[2] = props[\"p-bottom\"];\n }\n if (props[\"p-left\"] !== undefined) {\n paddings[3] = props[\"p-left\"];\n }\n widget.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);\n}\nfunction processTextProps(text, props) {\n switch (props.align) {\n case \"left\":\n text.leftAlignText();\n break;\n case \"right\":\n text.rightAlignText();\n break;\n case \"center\":\n text.centerAlignText();\n break;\n }\n if (props.font) {\n text.font = props.font;\n }\n if (props.color) {\n text.textColor = processJSXColor(props.color);\n }\n if (props.lineLimit) {\n text.lineLimit = props.lineLimit;\n }\n if (props.minimumScaleFactor) {\n text.minimumScaleFactor = props.minimumScaleFactor;\n }\n if (props.opacity) {\n text.textOpacity = props.opacity;\n }\n if (props.shadowColor) {\n text.shadowColor = processJSXColor(props.shadowColor);\n }\n if (props.shadowOffset) {\n text.shadowOffset = props.shadowOffset;\n }\n if (props.shadowRadius) {\n text.shadowRadius = props.shadowRadius;\n }\n if (props.url) {\n text.url = props.url;\n }\n}\nfunction processStackProps(stack, props) {\n switch (props.align) {\n case \"top\":\n stack.topAlignContent();\n break;\n case \"bottom\":\n stack.bottomAlignContent();\n break;\n case \"center\":\n stack.centerAlignContent();\n break;\n }\n if (props.backgroundColor) {\n stack.backgroundColor = processJSXColor(props.backgroundColor);\n }\n if (props.backgroundGradient) {\n stack.backgroundGradient = props.backgroundGradient;\n }\n if (props.backgroundImage) {\n stack.backgroundImage = props.backgroundImage;\n }\n if (props.borderColor) {\n stack.borderColor = processJSXColor(props.borderColor);\n }\n if (props.borderWidth) {\n stack.borderWidth = props.borderWidth;\n }\n if (props.cornerRadius) {\n stack.cornerRadius = props.cornerRadius;\n }\n if (props.size) {\n stack.size = props.size;\n }\n if (props.spacing) {\n stack.spacing = props.spacing;\n }\n if (props.url) {\n stack.url = props.url;\n }\n processPaddingProps(stack, props, [0, 0, 0, 0]);\n}\nfunction processImageProps(image, props) {\n switch (props.align) {\n case \"left\":\n image.leftAlignImage();\n break;\n case \"right\":\n image.rightAlignImage();\n break;\n case \"center\":\n image.centerAlignImage();\n break;\n }\n if (props.size) {\n image.imageSize = props.size;\n }\n if (props.borderColor) {\n image.borderColor = processJSXColor(props.borderColor);\n }\n if (props.borderWidth) {\n image.borderWidth = props.borderWidth;\n }\n if (props.containerRelativeShape) {\n image.containerRelativeShape = props.containerRelativeShape;\n }\n switch (props.contentMode) {\n case \"filling\":\n image.applyFillingContentMode();\n break;\n case \"fitting\":\n image.applyFittingContentMode();\n break;\n }\n if (props.cornerRadius) {\n image.cornerRadius = props.cornerRadius;\n }\n if (props.opacity) {\n image.imageOpacity = props.opacity;\n }\n if (props.resizable) {\n image.resizable = props.resizable;\n }\n if (props.tintColor) {\n image.tintColor = processJSXColor(props.tintColor);\n }\n if (props.url) {\n image.url = props.url;\n }\n}\nfunction processWidgetProps(widget, props) {\n if (props.backgroundColor) {\n widget.backgroundColor = processJSXColor(props.backgroundColor);\n }\n if (props.backgroundGradient) {\n widget.backgroundGradient = props.backgroundGradient;\n }\n if (props.backgroundImage) {\n widget.backgroundImage = props.backgroundImage;\n }\n if (props.refreshAfterDate) {\n widget.refreshAfterDate = props.refreshAfterDate;\n }\n if (props.spacing !== undefined) {\n widget.spacing = props.spacing;\n }\n if (props.url) {\n widget.url = props.url;\n }\n processPaddingProps(widget, props, [15, 15, 15, 15]);\n}\nfunction processContainerChildren(widget, children) {\n for (const child of children) {\n if (child === null || child === undefined || child === false ||\n typeof child === \"function\") {\n continue;\n }\n else if (typeof child === \"string\" || typeof child === \"number\" ||\n typeof child === \"bigint\" || typeof child === \"symbol\" || child === true) {\n widget.addText(String(child));\n }\n else if (typeof child === \"object\") {\n if (child instanceof Array) {\n processContainerChildren(widget, child);\n }\n else if (child instanceof Date) {\n widget.addDate(child);\n }\n else if (child instanceof Image) {\n widget.addImage(child);\n }\n else if (\"type\" in child) {\n switch (child.type) {\n case \"text\": {\n const text = widget.addText(child.text);\n processTextProps(text, child.props || {});\n break;\n }\n case \"date\": {\n const init = child.props.date;\n const date = widget.addDate(init instanceof Date ? init : new Date(init));\n switch (child.props.style) {\n case \"date\":\n date.applyDateStyle();\n break;\n case \"offset\":\n date.applyOffsetStyle();\n break;\n case \"time\":\n date.applyTimeStyle();\n break;\n case \"relative\":\n date.applyRelativeStyle();\n break;\n case \"timer\":\n date.applyTimerStyle();\n break;\n }\n processTextProps(date, child.props || {});\n break;\n }\n case \"stack\": {\n const stack = widget.addStack();\n switch (child?.props?.layout) {\n case \"vertical\":\n stack.layoutVertically();\n break;\n case \"horizontal\":\n stack.layoutHorizontally();\n break;\n }\n processStackProps(stack, child.props || {});\n processContainerChildren(stack, child.children);\n break;\n }\n case \"spacer\": {\n const spacer = widget.addSpacer();\n if (child?.props?.size) {\n spacer.length = child.props.length;\n }\n break;\n }\n case \"image\": {\n let init;\n const { data } = child.props || {};\n if (typeof data === \"string\") {\n init = Image.fromFile(child.props.fileURL);\n }\n else if (data.size) {\n init = data;\n }\n else {\n init = Image.fromData(data);\n }\n const image = widget.addImage(init);\n processImageProps(image, child.props || {});\n break;\n }\n }\n }\n }\n }\n}\nfunction widgetCreateElement(element, props, ...children) {\n if (typeof element === \"string\") {\n // intrinsic element\n switch (element) {\n case \"widget\":\n const widget = new ListWidget();\n processContainerChildren(widget, children);\n processWidgetProps(widget, props || {});\n return widget;\n case \"text\":\n let text = \"\";\n for (let child of children) {\n if (typeof child === \"string\" ||\n typeof child === \"number\" ||\n typeof child === \"bigint\") {\n text += child;\n }\n }\n return { type: element, props, text };\n case \"stack\":\n return { type: element, props, children };\n case \"spacer\":\n return { type: element, props };\n case \"image\":\n return { type: element, props };\n case \"date\":\n return { type: element, props };\n }\n }\n}\n\n// =============================\n// Alert JSX Element processing\n// =============================\nfunction processAlertProps(alert, props) {\n if (props.title) {\n alert.title = props.title;\n }\n if (props.message) {\n alert.message = props.message;\n }\n}\nfunction processActionProps(alert, props, children) {\n const text = props.text || (children instanceof Array ? children.join(\" \") : children) || \"\";\n switch (props.type || \"action\") {\n case \"action\": {\n alert.addAction(text);\n break;\n }\n case \"cancel\": {\n alert.addCancelAction(text);\n break;\n }\n case \"destructive\": {\n alert.addDestructiveAction(text);\n break;\n }\n }\n}\nfunction processTextFieldProps(alert, props) {\n (props.secure ? alert.addSecureTextField : alert.addTextField)(props.placeholder || \"\", props.text || \"\");\n}\nfunction processAlertChildren(alert, children) {\n for (const child of children) {\n if (typeof child === \"string\") {\n return child;\n }\n if (typeof child === \"object\") {\n if (child instanceof Array) {\n processAlertChildren(alert, child);\n }\n else if (\"type\" in child) {\n switch (child.type) {\n case \"action\": {\n processActionProps(alert, child.props || {}, child.children || \"\");\n break;\n }\n case \"text-field\": {\n processTextFieldProps(alert, child.props || {});\n break;\n }\n }\n }\n }\n }\n}\nfunction alertCreateElement(element, props, ...children) {\n if (typeof element === \"string\") {\n // intrinsic element\n switch (element) {\n case \"alert\":\n const alert = new Alert();\n processAlertChildren(alert, children);\n processAlertProps(alert, props || {});\n return alert;\n case \"text-field\":\n return { type: element, props };\n case \"action\":\n return { type: element, props, children };\n }\n }\n}\n\nconst createElements = [widgetCreateElement, alertCreateElement];\nclass ScriptableJSX {\n static createElement(element, props, ...children) {\n for (const creator of createElements) {\n const res = creator(element, props, ...children);\n if (res) {\n return res;\n }\n }\n }\n}\n\n// =========================================\n// Constants\n// =========================================\nconst HASS_URL = \"HASS_URL\";\nconst HASS_API_TOKEN = \"HASS_TOKEN\";\n// =========================================\n// Colors\n// =========================================\n// language=CSS prefix=*{color:# suffix=;}\nconst BackgroundColor = Color.dynamic(new Color('ccc', 1), new Color('1c1c1c', 1));\n// language=CSS prefix=*{color:# suffix=;}\nconst SecondBackgroundColor = Color.dynamic(new Color('000', .1), new Color('fff', .1));\n// language=CSS prefix=*{color:# suffix=;}\nconst SecondActiveBackgroundColor = Color.dynamic(new Color('000', .2), new Color('fff', .2));\n// language=CSS prefix=*{color:# suffix=;}\nconst TextColor = Color.dynamic(new Color('000', 1), new Color('fff', 1));\n// language=CSS prefix=*{color:# suffix=;}\nconst TransparentColor = new Color('000', 0);\n\nfunction baseUrl(baseUrl) {\n return baseUrl.replace(/\\/?$/, '');\n}\nfunction createRequest(url, token) {\n const req = new Request(url);\n req.headers = { 'Authorization': `Bearer ${token || Keychain.get(HASS_API_TOKEN)}` };\n return req;\n}\nasync function getAPIStatus(url, token) {\n const apiCheckUrl = (url ? baseUrl(url) : Keychain.get(HASS_URL)) + '/api/';\n const req = createRequest(apiCheckUrl, token);\n console.log(`Checking ${apiCheckUrl}`);\n const d = await req.loadString();\n console.log(`d: ${d}`);\n const data = JSON.stringify(JSON.parse(d));\n console.log(`data: ${data}`);\n return data === JSON.stringify({ \"message\": \"API running.\" });\n}\nasync function getAllStates() {\n return await createRequest(Keychain.get(HASS_URL) + '/api/states').loadJSON();\n}\nasync function getPersonStates() {\n const states = await getAllStates();\n return states.filter(state => state.entity_id.startsWith('person.'));\n}\nasync function getPicture(person) {\n return await createRequest(Keychain.get(HASS_URL) + person.attributes.entity_picture).loadImage();\n}\nfunction createPersonURL(person) {\n return `${Keychain.get(HASS_URL)}/history?entity_id=${person.entity_id}`;\n}\nasync function join(array, callback, separator) {\n if (!separator) {\n separator = ScriptableJSX.createElement(\"spacer\", null);\n }\n const result = [];\n const length = array.length;\n for (let i = 0; i < length; i++) {\n const value = array[i];\n const out = await callback(value);\n if (!out) {\n break;\n }\n result.push(out);\n if (i !== length - 1)\n result.push(separator);\n }\n return result;\n}\n\nfunction getKeychain(key) {\n if (Keychain.contains(key)) {\n return Keychain.get(key);\n }\n}\nasync function login(url, token) {\n let argsPassed = true;\n if (!(url && token)) {\n if (!(Keychain.contains(HASS_URL) && Keychain.contains(HASS_API_TOKEN))) {\n return false;\n }\n url = getKeychain(HASS_URL);\n token = getKeychain(HASS_API_TOKEN);\n argsPassed = false;\n }\n if (url && token) {\n const resp = await getAPIStatus(url, token);\n if (!resp) {\n return false;\n }\n if (config.runsInApp && argsPassed) {\n const a = (ScriptableJSX.createElement(\"alert\", { title: \"Login Successful!\", message: \"Now you can use the widget!\" },\n ScriptableJSX.createElement(\"action\", { type: \"cancel\" }, \"Close\")));\n await a.present();\n }\n console.log(`Success login!`);\n console.log(`${url}, ${token}`);\n Keychain.set(HASS_URL, baseUrl(url));\n Keychain.set(HASS_API_TOKEN, token);\n return true;\n }\n return false;\n}\nasync function loginMenu() {\n const a = (ScriptableJSX.createElement(\"alert\", { title: \"Login in HomeAssistant\" },\n ScriptableJSX.createElement(\"text-field\", { placeholder: 'Full URL to HomeAssistant URL', text: getKeychain(HASS_URL) }),\n ScriptableJSX.createElement(\"text-field\", { placeholder: 'HomeAssistant Token', text: getKeychain(HASS_URL), secure: true }),\n ScriptableJSX.createElement(\"action\", { type: \"cancel\" }, \"Cancel\"),\n ScriptableJSX.createElement(\"action\", null, \"Login\")));\n let res = await a.present();\n console.log(`${res}, ${a.textFieldValue(0)}, ${a.textFieldValue(1)}`);\n const [url, token] = [a.textFieldValue(0), a.textFieldValue(1)];\n return await login(url, token);\n}\n\n// =========================================\n// Widgets\n// =========================================\nasync function loginWidget() {\n return (ScriptableJSX.createElement(\"widget\", { backgroundColor: BackgroundColor },\n ScriptableJSX.createElement(\"text\", null, \"Before using Widget, please login!\")));\n}\nasync function personsWidget() {\n const persons = await getPersonStates();\n let personGrid = [];\n switch (displayMode()) {\n case \"small\":\n personGrid = [\n [persons[0], persons[1]],\n [persons[2], persons[3]],\n ];\n break;\n case \"medium\":\n personGrid = [\n [persons[0], persons[1], persons[2], persons[3]],\n ];\n break;\n case \"large\":\n case \"extraLarge\":\n personGrid = [\n [persons[0], persons[1], persons[2], persons[3]],\n [persons[4], persons[5], persons[6], persons[7]],\n [persons[8], persons[9], persons[10], persons[11]],\n [persons[12], persons[13], persons[14], persons[15]],\n ];\n break;\n }\n const widgetTitleText = {\n font: Font.title3(),\n color: TextColor,\n };\n const widgetTitle = FULL_CARD() ? [\n ScriptableJSX.createElement(\"stack\", { layout: \"horizontal\" },\n ScriptableJSX.createElement(\"text\", { ...widgetTitleText }, WIDGET_TITLE),\n ([\n ScriptableJSX.createElement(\"spacer\", null),\n ScriptableJSX.createElement(\"text\", { ...widgetTitleText },\n persons.filter(p => p.state === 'home').length,\n \"/\",\n persons.length)\n ])),\n ScriptableJSX.createElement(\"spacer\", null)\n ] : [];\n const jsxPersonsGrid = await join(personGrid, async (row) => (ScriptableJSX.createElement(\"stack\", { layout: \"horizontal\" }, await join(row, personCard))));\n return (ScriptableJSX.createElement(\"widget\", { backgroundColor: BackgroundColor },\n widgetTitle,\n ScriptableJSX.createElement(\"stack\", { layout: \"vertical\" }, jsxPersonsGrid)));\n}\nasync function personCard(person) {\n if (!person)\n return ScriptableJSX.createElement(\"stack\", null);\n const at_home = person.state === 'home';\n const opacity = at_home ? 1 : 0.5;\n const image = await getPicture(person);\n const fontSize = 14;\n const props = (FULL_CARD() ?\n {\n spacing: 5,\n 'p-all': 7,\n cornerRadius: 15,\n backgroundColor: at_home ? SecondActiveBackgroundColor : SecondBackgroundColor\n } : {});\n const friendlyName = (ScriptableJSX.createElement(\"stack\", { layout: \"horizontal\" },\n ScriptableJSX.createElement(\"spacer\", null),\n ScriptableJSX.createElement(\"text\", { color: TextColor, font: at_home ? Font.mediumSystemFont(fontSize) : Font.regularSystemFont(fontSize), opacity: opacity }, person.attributes.friendly_name),\n ScriptableJSX.createElement(\"spacer\", null)));\n return (ScriptableJSX.createElement(\"stack\", { layout: \"vertical\", ...props, url: createPersonURL(person) },\n ScriptableJSX.createElement(\"image\", { data: image, align: \"center\", opacity: opacity, cornerRadius: image.size.width / 2, borderWidth: 5, borderColor: at_home ? TextColor : TransparentColor, resizable: true }),\n FULL_CARD() && friendlyName));\n}\n\nlet loggedIn = await login();\nconst widget = loggedIn ? personsWidget : loginWidget;\nif (config.runsInWidget) {\n Script.setWidget(await widget());\n}\nelse if (config.runsInApp) {\n if (loggedIn) {\n // Showing all widgets\n displayMode('small');\n const widgetSmall = await widget();\n await widgetSmall.presentSmall();\n displayMode('medium');\n const widgetMedium = await widget();\n await widgetMedium.presentMedium();\n displayMode('large');\n const widgetLarge = await widget();\n await widgetLarge.presentLarge();\n displayMode('extraLarge');\n const widgetExtraLarge = await widget();\n await widgetExtraLarge.presentExtraLarge();\n }\n else {\n await loginMenu();\n }\n}\nScript.complete();"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment