Widget Editor
The View (HTML)

Open media
Forget complex setups. Unlike other platforms that demand local development environments and command-line tools, HomeGenie offers a direct and simple approach. If you know basic HTML, CSS, and JavaScript, you're ready to start creating. There’s nothing to install because your browser and the HomeGenie UI are your complete development environment.
Each widget is a self-contained component, giving you total creative freedom. This simplicity doesn't limit power. Your widgets can be anything from interactive front-ends for your physical devices, to elegant dashboards for services like a weather forecast, or even custom interfaces for your own automation programs.
This guide will show you how to harness this power, helping you build fully encapsulated, theme-aware, and easily shareable widgets that will make your smart system truly your own.
Every widget is composed of three parts, which you will find in the editor tabs:
Every widget is rendered inside its own Shadow DOM. Think of it as a protective "bubble" created by native browser technology. This isolation ensures that your widget's styles and scripts can never accidentally interfere with the main HomeGenie application (or other widgets), giving you a completely safe and stable environment to build in.
The JavaScript file is the brain of your widget. Here, you'll define a class that extends ControllerInstance
to manage its behavior, handle data, and respond to user input.
Your controller class includes several special methods, called "lifecycle hooks", that HomeGenie automatically runs at specific moments. This gives you fine-grained control over your widget's setup and teardown.
onInit()
: Runs before the HTML is rendered. Use this for pre-setup tasks that don’t rely on the UI, like setting up data subscriptions from boundModule
. Important: you cannot access the data model here, as it hasn't been created yet.
onCreate()
: Runs after the HTML is rendered and the data model is ready. This is your main setup method. Use it to interact with the model for the first time, populate it with live data, and load any view-dependent resources.
onDispose()
: Runs just before the widget is destroyed. Use this to clean up subscriptions, timers, or other resources to prevent memory leaks.
onUpdate(target, key, value, path, old)
: A powerful but specialized hook that runs anytime a property of the data model changes. Useful for advanced scenarios where you need to react to any model change and user input.
this.model
)The core of a dynamic widget is its data model, this.model
. It acts as the bridge between your UI (HTML) and your logic (JavaScript). HomeGenie creates this bridge for you automatically.
Declaration in HTML: Any element with a #field_name
attribute automatically creates a property on the data model. The element's initial content becomes the property's initial value.
<!-- This creates `this.model().name` with the value "My Device" -->
<div #name>My Device</div>
Access in JavaScript: Starting from the onCreate
hook, you can access and modify the model object using this.model()
. Any changes you make will be automatically reflected in the UI.
// Inside onCreate() or other methods
this.model().name = 'New Device Name';
this.boundModule
: The gateway to HomeGenie's dataWhen you link a widget to a module on the dashboard, the controller receives the this.boundModule
property. This object is your direct line to read data from and send commands to any HomeGenie entity.
While this is often a physical device like a light or a sensor, a boundModule
can also represent a virtual service or a custom data source from an automation program.
This concept is what makes HomeGenie widgets so powerful. It means you can write an automation program that fetches data from a web API, performs complex calculations, or monitors a system service, and then presents that logic as a virtual module. This custom module can expose its own unique properties (fields
) and commands.
A perfect example is the built-in Weather Forecast service. There is no physical weather device, but an automation program fetches data online and presents it as a standard module. This module has fields like Sensor.Temperature
that your widget can bind to, just as it would with any physical device.
This opens up limitless possibilities: your widget can become the custom UI for virtually any data source you can integrate through HomeGenie's automation engine.
The boundModule
object provides several key tools:
name
, id
, type
, description
, fields
(a list of all module fields).field(key)
: Retrieves a specific ModuleField
object by its key (e.g., 'Status.Level'
).control(command, options)
: Sends a command to the module and returns an object you can subscribe
to for tracking completion.this.subscribe(field, handler)
To make your widget react to live data, this.subscribe
is your primary tool. It safely listens for changes from a ModuleField
and automatically handles cleanup when the widget is destroyed, preventing common memory leaks.
// In onInit(), set up the subscription
const levelField = this.boundModule.field('Status.Level');
if (levelField) {
this.subscribe(levelField, (updatedField) => {
// This callback runs every time the value changes
this.model().level_percent = updatedField.value * 100;
});
}
utils
global toolboxTo simplify common tasks, HomeGenie provides a global utils
object packed with helper functions.
utils.format
: Displays data consistently with the user's preferences.
fieldValue(field)
: Formats a value based on the user's unit settings (e.g., °C vs °F).fieldName(field)
: Returns a clean, human-readable name for a field.utils.convert
: A powerful library for unit conversions.
// Convert 10 seconds to milliseconds
const ms = utils.convert.time(10).from('s').to('ms').value; // 10000
utils.ui
: Displays notifications and tooltips in the main UI.
notify(title, message, options)
tooltip(message, options)
utils.preferences
: Gives you read-only access to UI settings like the current theme and language.
Your widget can automatically adapt to the current HomeGenie theme by using standard CSS variables in your stylesheet.
.slider {
color: var(--accent-color);
}
Common variables include: --primary-color
, --accent-color
, --warn-color
, --base-color
, --text-color
, --background-color
, and --card-color
.
Why design from scratch? One of the most powerful features of HomeGenie is the ability to leverage a vast world of open-source UI libraries to create professional-looking widgets in seconds. Because widgets are built on standard HTML and CSS, they are directly compatible with thousands of free, community-contributed components available online.
The process is incredibly simple and takes less than a minute.
That's it. In most cases, the widget will work instantly, no changes needed. This is a fantastic way to achieve a high-quality, polished look for your dashboard without needing to be a design expert. It allows you to stand on the shoulders of talented designers from around the world.
Let's build a practical, real-world dimmer switch widget. This example ties everything together and teaches you best practices for lifecycle management, data-binding, event handling, theming, and resource loading. Each part of the code is explained in detail below.
The HTML does more than just define the structure; it's the blueprint for our widget's reactive data model. It declares data fields with the #
syntax, which serve as placeholders, and connects user actions (like moving a slider) directly to functions in our JavaScript controller.
<div class="container">
<div class="header">
<strong #name>Dimmer</strong>
<span class="level-text"><span #level_percent>50</span>%</span>
</div>
<div class="slider-container">
<span class="material-symbols-outlined">light</span>
<input
type="range"
min="0"
max="100"
class="slider"
@sync #level_percent
(input)="setLevel(this.value)"
/>
</div>
</div>
<!-- Material Design Symbols and Icons CSS -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">
#name
and #level_percent
: These attributes create two properties on our data model, name
and level_percent
, initializing them with the default text content ("Dimmer" and "50").@sync #level_percent
: This powerful zuix.js
directive creates a two-way data-binding. It means the slider's position and the model.level_percent
variable are now permanently linked. If one changes, the other updates automatically.(input)="setLevel(this.value)"
: This is a classic event handler. It connects a user action (moving the slider) directly to the setLevel
function in our controller, passing the slider's current value.<link ...>
: This loads the external stylesheet for Material Symbols. Because it's placed inside our widget's template, it gets loaded safely within the Shadow DOM, ensuring it can't interfere with the main application or other widgets.The styling makes our widget look clean and, more importantly, theme-aware. By using CSS variables provided by the HomeGenie UI, our widget will automatically adapt to the user's current theme for a seamless, native look.
:host {
font-family: sans-serif;
min-width: 312px;
}
.container {
padding: 16px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 1.1em;
}
.level-text {
color: var(--text-color);
margin-right: 24px;
}
.slider-container {
display: flex;
align-items: center;
gap: 12px;
}
.slider {
flex-grow: 1;
accent-color: var(--accent-color);
}
:host
: This special CSS selector targets the root element of our widget, allowing us to define its base styles and dimensions.var(--text-color)
, var(--accent-color)
: Using these variables ensures our widget's text and slider colors will automatically match the user's selected theme (e.g., light or dark).This is the brain of our widget. The controller's job is to fetch the initial device state, listen for real-time updates, and send commands back to the device when the user interacts with the UI. The logic is carefully split between the onInit
and onCreate
lifecycle hooks for optimal performance.
class DimmerWidget extends ControllerInstance {
busy = false;
levelRetry = null;
onInit() {
// Expose the setLevel method to be callable from the HTML template
this.declare({
setLevel: (l) => this.setLevel(l)
});
// If no module is bound to this widget, do nothing else
if (!this.boundModule) {
return;
}
// Subscribe to future changes of the module's level field.
const levelField = this.boundModule.field('Status.Level');
if (levelField) {
this.subscribe(levelField, (field) => {
// When a change is received, update our model. The UI will update automatically.
this.model().level_percent = Math.round(field.value * 100);
});
}
}
onCreate() {
if (!this.boundModule) {
this.model().name = 'No module bound';
return;
}
// The UI and model now exist.
// Sync the initial state from the module to the model.
const levelField = this.boundModule.field('Status.Level');
const initialValue = levelField ? levelField.value : 0; // Normalized 0.0-1.0
this.model().name = this.boundModule.name;
this.model().level_percent = Math.round(initialValue * 100);
}
setLevel(percentValue) {
if (!this.boundModule) return;
// A simple retry mechanism to handle rapid slider movements
if (this.busy) {
clearTimeout(this.levelRetry);
this.levelRetry = setTimeout(() => this.setLevel(percentValue), 100);
return;
}
// Optimistically update the UI model for instant user feedback
this.model().level_percent = percentValue;
// Send the command and lock the control
this.busy = true;
this.boundModule.control('Control.Level', percentValue).subscribe(() => {
this.busy = false; // Unlock only after the command is acknowledged
});
}
}
onInit()
: This hook runs before the UI exists. Its main job is to prepare logic that doesn't depend on UI elements. Here, we declare
methods to make them accessible from the HTML and subscribe
to data sources to listen for future updates.onCreate()
: This hook runs after the UI and data model have been created. This is the perfect moment to populate the UI with real data for the first time, syncing the widget's state with the actual device's state.setLevel()
: This function is called every time the user moves the slider. It performs three key tasks:busy
and levelRetry
logic is a robust mechanism to prevent flooding the network with commands. It waits for the user to pause before sending the final value.this.model().level_percent = percentValue
provides instant visual feedback, making the UI feel responsive even before the device confirms the change.this.boundModule.control(...)
sends the actual command. We lock the state with this.busy = true
and only unlock it in the subscribe
callback, ensuring we don't send a new command until the last one is acknowledged.This example takes the next step, showing you how to build a widget for a module with a rich, complex dataset: the built-in Weather Forecast automation program. It showcases how to handle multiple data points from a single source. This example is a showcase of best practices for lifecycle management, centralized data synchronization, and creating a theme-aware style that integrates perfectly with the main application.
The HTML defines a comprehensive data model for our widget. We create placeholders for every piece of data we want to display, from the city name to the temperature for a specific forecast day.
<div class="container">
<!-- Current Weather Section -->
<div class="current-weather">
<div class="icon-container">
<span class="material-symbols-outlined main-icon" #weather_icon>thermostat</span>
</div>
<div class="details">
<div class="city" #city>City</div>
<div class="temperature"><span #temperature>0</span>°</div>
<div class="description" #description>Description</div>
</div>
</div>
<!-- Forecast Section -->
<div class="forecast-container">
<div class="forecast-day">
<div #forecast_1_day>Day 1</div>
<span class="material-symbols-outlined forecast-icon" #forecast_1_icon>wb_sunny</span>
<div><strong #forecast_1_temp_max>0</strong>° / <span #forecast_1_temp_min>0</span>°</div>
</div>
<div class="forecast-day">
<div #forecast_2_day>Day 2</div>
<span class="material-symbols-outlined forecast-icon" #forecast_2_icon>wb_sunny</span>
<div><strong #forecast_2_temp_max>0</strong>° / <span #forecast_2_temp_min>0</span>°</div>
</div>
</div>
</div>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">
#weather_icon
, #city
, etc.: Each #
attribute defines a property on our data model, using the tag's content ("City", "0") as a temporary initial value until real data is loaded.thermostat
) directly in the HTML. This ensures the widget doesn't look broken or empty while waiting for the initial data fetch.<link ...>
: As in the previous example, this tag safely loads an external stylesheet inside the widget's Shadow DOM, preventing any style conflicts with the main application.The CSS is where we give our widget its unique look and feel. We use Flexbox for layout and theme variables to ensure it integrates seamlessly into the HomeGenie UI.
:host {
font-family: sans-serif;
}
.container {
padding: 16px;
}
.current-weather {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--divider-color);
}
.main-icon {
font-size: 64px;
color: var(--primary-color);
margin-top: 6px;
}
.city {
font-size: 1.2em;
font-weight: bold;
}
.temperature {
font-size: 2em;
margin-top: 12px;
}
.forecast-container {
display: flex;
justify-content: space-around;
padding-top: 12px;
}
.forecast-day {
text-align: center;
font-size: 0.9em;
}
.forecast-icon {
font-size: 48px;
margin: 4px 0;
}
:host
: Targets the widget's root element to set a consistent base font.var(--divider-color)
, var(--primary-color)
: By using these CSS variables, our widget automatically adapts to the user's theme (light/dark), ensuring elements like icons and borders always have the right color.The controller's main job here is to take the complex data from the weather module and map it to our simple data model. This is achieved using a clean, centralized updateModel()
method, which acts as the single source of truth for syncing the UI.
class WeatherWidget extends ControllerInstance {
onInit() {
// This hook runs before the view is loaded. It's the ideal place
// to set up subscriptions that will update the model once it exists.
if (!this.boundModule) {
return;
}
// We only subscribe to one key field. When it changes, we assume all
// weather data has been updated and trigger a full refresh.
this.subscribe(this.boundModule.field('Sensor.Temperature'), (updatedField) => {
this.updateModel();
});
}
onCreate() {
// The UI is now ready. We perform the initial data sync.
if (!this.boundModule) {
this.model().city = 'No module bound';
return;
}
this.updateModel();
}
/**
* Performs a full sync of the widget's model with the bound module's state.
* This centralized method is the single source of truth for updating the UI.
*/
updateModel() {
const m = this.model();
this.boundModule.fields.forEach((f) => {
switch (f.key) {
case 'Conditions.City': m.city = f.value; break;
case 'Sensor.Temperature': m.temperature = f.value; break;
case 'Conditions.Description': m.description = f.value; break;
case 'Conditions.IconType': m.weather_icon = this.getMaterialIconName(f.value); break;
// Forecast Day 1
case 'Conditions.Forecast.1.IconType': m.forecast_1_icon = this.getMaterialIconName(f.value); break;
case 'Conditions.Forecast.1.Weekday': m.forecast_1_day = f.value; break;
case 'Conditions.Forecast.1.Temperature.Max': m.forecast_1_temp_max = f.value; break;
case 'Conditions.Forecast.1.Temperature.Min': m.forecast_1_temp_min = f.value; break;
// Forecast Day 2
case 'Conditions.Forecast.2.IconType': m.forecast_2_icon = this.getMaterialIconName(f.value); break;
case 'Conditions.Forecast.2.Weekday': m.forecast_2_day = f.value; break;
case 'Conditions.Forecast.2.Temperature.Max': m.forecast_2_temp_max = f.value; break;
case 'Conditions.Forecast.2.Temperature.Min': m.forecast_2_temp_min = f.value; break;
}
});
}
/**
* Maps a weather API icon code to a Material Symbol icon name.
* @param {string} iconCode - E.g., "01d", "04n", "10d".
* @returns {string} The corresponding Material Symbol name.
*/
getMaterialIconName(iconCode) {
if (!iconCode) return 'help';
const code = iconCode.substring(0, 2); // '01d' -> '01'
switch (code) {
case '01': return 'sunny';
case '02': return 'partly_cloudy_day';
case '03': case '04': return 'cloudy';
case '09': case '10': return 'rainy';
case '11': return 'thunderstorm';
case '13': return 'ac_unit'; // Snow
case '50': return 'foggy'; // Mist
default: return 'thermostat';
}
}
}
onInit()
: The strategy here is efficiency. Instead of subscribing to a dozen different fields, we pick one key field (Sensor.Temperature
) to act as a "heartbeat". When it updates, we assume the entire weather report is new and trigger a full refresh by calling updateModel()
.onCreate()
: Once the UI is built, we call updateModel()
immediately to perform the first-time sync, replacing the placeholder content with live data from the module.updateModel()
: This is the core of our widget. It's a centralized function that acts as the single source of truth for the UI. It systematically maps each piece of data from the module to the corresponding property in our model. This approach keeps the logic clean, organized, and easy to debug.getMaterialIconName()
: This helper function is a great example of separating concerns. It neatly encapsulates the logic for translating technical API codes (like 10d
) into user-friendly icon names (rainy
), keeping the main updateModel()
method cleaner and focused on its primary task of data mapping.Your creations aren't locked into your system. Widgets are designed to be portable, making it easy to back them up, move them between installations, and share them with the HomeGenie community.
Exporting Your Work: With a single click, you can export any widget. This process bundles its HTML, CSS, and JavaScript into a single, convenient .zip
file, ready to be shared.
Importing from the Community: To use a widget from someone else, just import its .zip
file. HomeGenie handles the rest, automatically installing it into your editor and making it instantly available to use and customize on your dashboard.
This simple system empowers a collaborative community. It allows you to share your best creations and build upon the amazing work of others, making everyone's smart home more powerful and personalized.
This guide covers the essentials for building custom widgets in HomeGenie. The underlying component framework, zuix.js
, offers many more advanced features. For a deep dive into all capabilities, please refer to the official zuix.js documentation.