Custom Widgets

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.

The core concept: The three pillars of a widget

Every widget is composed of three parts, which you will find in the editor tabs:

  1. HTML (The Structure): Defines the content and layout of your widget—every button, label, and container. It also sets up the initial data your widget will use.
  2. CSS (The Style): Controls the visual appearance, from colors and fonts to spacing and animations.
  3. JavaScript (The Logic): Brings your widget to life by handling user interactions, processing data, and communicating with your devices.

Widget Editor

The Controller (JavaScript)


Open media

Isolation and safety: The power of shadow DOM

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 widget controller

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.

Lifecycle methods

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.

The Data Model (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.

  1. 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>
  2. 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 data

When 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:

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;
  });
}

The utils global toolbox

To simplify common tasks, HomeGenie provides a global utils object packed with helper functions.

Theme-aware styling with CSS variables

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.

Create stunning widgets in seconds with UI libraries

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.

  1. Find a Component: Browse a community site like UIverse.io and find a design you love.
  2. Copy the HTML: Paste its HTML structure into the HTML tab of the widget editor.
  3. Copy the CSS: Paste its corresponding CSS into the Style tab.

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.

Example 1: A data-driven dimmer switch

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.

HTML

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">

CSS

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);
}

JavaScript

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
    });
  }
}

Example 2: A weather widget

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.

HTML

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">

CSS

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;
}

JavaScript

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';
    }
  }
}

Portability and sharing: Exporting and importing widgets

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.

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.

Download example widgets in this page

light Dimmer Widget
partly_cloudy_day Weather Widget

Further reading

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.

menu_open Content index
forum Q & A discussion forum
HomeGenie
SERVER 2.0 — Documentation