Python

Using Python in HomeGenie programs

Python is a widely popular programming language known for its clear syntax and extensive libraries, making it an excellent choice for scripting automation logic. It fits perfectly within the HomeGenie ecosystem, allowing developers to create custom automations, integrate external services, or process data in sophisticated ways.

A key aspect of Python support in HomeGenie is its use of IronPython as the scripting engine. IronPython is an implementation of Python that runs on the .NET framework, the same foundation HomeGenie itself is built upon. This tight integration provides a significant advantage: you don't need to install Python separately on the machine running HomeGenie. The necessary IronPython engine is bundled, allowing your Python scripts to directly interact with the HomeGenie API (hg).

API usage examples

Let's explore how to use the HomeGenie API within a Python program.

Adding modules

In HomeGenie, a module represents any controllable entity – a physical device, a virtual sensor, a service, or even data generated by a program itself. Programs can dynamically create new modules to expose information or control points to the rest of the system.

The following example demonstrates adding modules using Python on a Linux system to monitor the temperature of internal hardware sensors. The code utilizes the hg.Program.AddModule API method to create a distinct "Sensor" module for each detected thermal zone.

# Main Code Block - Initial Setup Part

import subprocess

# Linux commands to read thermal zone types (labels) and temperatures
proc_thermal_type = "cat /sys/class/thermal/thermal_zone*/type"
proc_thermal_temp = "cat /sys/class/thermal/thermal_zone*/temp"

# Get sensor labels
output = subprocess.check_output(proc_thermal_type, shell=True)
output = output.decode("utf-8")
labels = output.split("\n")

# Initialize the array of sensor modules
sensor = []

# Add a module for each sensor
domain = "Components.Core" # Assign a logical domain for these modules
for i in range(len(labels) - 1): # Iterate through labels (skip last empty item)
  l = labels[i] = labels[i].upper()
  # the type "Sensor" also determines the widget
  # to be used to display this module in the UI
  hg.Program.AddModule(domain, l, "Sensor")
  sensor.append(hg.Modules.InDomain(domain).WithAddress(labels[i]).Get());
Emitting data/values

Following the initial setup logic within the same Main block, the program then typically enters its primary operational loop. In this loop, it periodically reads the current sensor values and uses the Emit method on the previously obtained module helper objects to publish the data. When a module emits a value, HomeGenie makes this data available system-wide for logging, visualization, and use by other automations or dashboards.

# Main Code Block - Continuous Loop Part

# Read and emit temperature values
while (hg.Program.IsRunning):
  output = subprocess.check_output(proc_thermal_temp, shell=True)
  output = output.decode("utf-8")
  values = output.split("\n")

  for i in range(len(labels) - 1):
    t = (int(values[i]) / 1000);
    # Emit the temperature value using the specific module helper
    # The parameter name "Sensor.Temperature" is standard for temperature sensors
    sensor[i].Emit("Sensor.Temperature", t)

  # Pause for 1 second before the next reading cycle
  hg.Pause(1)

Once a module is programmatically added via hg.Program.AddModule, it automatically integrates with standard HomeGenie features like historical data logging, real-time visualization in the Data Monitor, data processing rules, and availability within dashboards and other automations.

You can see this Python example in action in the video below.

Note on standard types and properties: HomeGenie uses conventional names for common module types (e.g., Sensor, Light, Switch) and the data properties they emit (e.g., Sensor.Temperature, Status.Level). Using these standards ensures compatibility with built-in widgets and features. A detailed reference list will be provided in a dedicated section or appendix.

Adding a custom API handler

Sometimes, you need to expose custom functionality from your Python program so it can be triggered externally via a simple web request (HTTP GET or POST). This is useful for integrating with other systems, creating custom endpoints for webhooks, or allowing dashboards or other programs to interact with services managed by your Python script. HomeGenie provides the hg.Api.Handle method for this.

You register a specific URL path pattern and associate it with a Python function (handler_fn in the example below). When HomeGenie receives an incoming API request matching that pattern, it calls your function, passing the request details. Your function processes the request and returns a response (often a simple string or a JSON formatted string).

# Main Code Block (typically no loop needed for just API handlers)

# Define standard responses for convenience
res_ok = '{"ResponseStatus": "OK"}'
res_err = '{"ResponseStatus": "ERROR"}'

# This is the function that will process incoming API requests
def handler_fn(request):
  # Parse the raw request into a structured object for easier access
  req = hg.Api.Parse(request)
  hg.Program.Log.Trace("API Handler received command: " + req.Command)

  # req object properties:
  # req.Domain         - The first part of the path ("My.Domain" in this registration)
  # req.Address        - The second part of the path ("TestModule" in this registration)
  # req.Command        - The part of the path *after* domain/address
  # req.Data           - Posted data (body) object, if the request was a POST
  # req.OriginalString - The full original request URL path (after /api/)
  # req.GetOption(n)   - The nth path segment *after* the command (0-based index)

  # Route the request based on the command
  if req.Command == "ping":
      return "PONG" # Return a simple string response
  if req.Command == "Control.On":
    hg.Program.Log.Info("Received Control.On command")
    # TODO: Add actual code here to turn ON the device/service
    return res_ok # Return a JSON success status
  if req.Command == "Control.Off":
    hg.Program.Log.Info("Received Control.Off command")
    # TODO: Add actual code here to turn OFF the device/service
    return res_ok # Return a JSON success status

  # If the command wasn't recognized, return an error
  hg.Program.Log.Warning("Unknown API command received: " + req.Command)
  return res_err # Return a JSON error status

# Register the handler function for the specified API path prefix
# Any request starting with /api/My.Domain/TestModule/ will trigger handler_fn
hg.Api.Handle("My.Domain/TestModule", handler_fn)
hg.Program.Log.Info("Custom API Handler registered for My.Domain/TestModule")

# Since this program only needs to register the handler and doesn't need
# an active loop, GoBackground allows the main script execution to end
# while keeping the API handler active and listening for requests.
hg.Program.GoBackground()

With this program running, calling the API endpoint via HTTP requests works as follows:

Making a GET request to:

http://<hg_address>/api/My.Domain/TestModule/Control.On

will execute the Control.On logic in handler_fn and return the JSON string {"ResponseStatus": "OK"}.

Making a GET request to:

http://<hg_address>/api/My.Domain/TestModule/ping

will execute the ping logic and return the plain text string PONG.

Making an API call

Just as programs can expose APIs, they can also call APIs provided by HomeGenie itself or other programs and modules. This is the primary way programs interact with each other and control devices. The hg.Api.Call method is used for this purpose.

You provide the target API path (the part after /api/) and optionally data to be sent (for POST requests).

# Example: Turning on a virtual light module with domain "Virtual.Light" and address "Lamp1"
try:
  hg.Program.Log.Info("Attempting to turn on Lamp1")
  # Target path format: <Domain>/<Address>/<Command>
  response = hg.Api.Call("My.Domain/TestModule/Control.On")
  hg.Program.Log.Info("API Call Response: " + str(response))
except Exception as e:
  hg.Program.Log.Error("Error calling API: " + str(e))

A significant optimization occurs for local API calls (calls made within the same HomeGenie instance). Instead of routing through the full HTTP network stack, HomeGenie performs these calls directly in memory. This is much faster. Furthermore, if the called API handler is also running within the same HomeGenie instance and is designed to handle complex Python objects (lists, dictionaries, custom classes), hg.Api.Call can pass this data directly without needing JSON serialization/deserialization. The receiving handler gets the original Python object, allowing for maximum performance and data fidelity for inter-program communication within the same system.

Adding new module features

As explained in the Programming Introduction page with the Blink feature example, programs can dynamically add new features or settings directly to specific types of modules (like lights, sensors, switches). This allows users to enable or configure specialized behavior on a per-device basis. The primary API method for this is hg.Program.AddFeature.

When you call hg.Program.AddFeature, you define which modules it applies to, a unique name, a description, and the type of UI control. HomeGenie then automatically adds this control to the Settings dialog of every matching module. Your program logic can later identify and interact with modules where the feature is active using various API methods.

# Example: Add a simple "Enable Logging" checkbox feature to all Switch modules

# Typically placed at the start of the Main block or in Setup if using hg.Program.Run()
hg.Program.AddFeature(
  "",                                         # Domain filter (empty = all domains)
  "Switch",                                   # Type filter (only apply to Switches)
  "MyFeatures.ExtraLogging.Enable",           # Unique feature field name
  "Enable detailed logging for this switch.", # Description for user
  "checkbox"                                  # UI control type
)

# If the program's only job is to register this feature, let it exit cleanly.
# The feature registration persists until the program is disabled or deleted.
# If the program also *uses* the feature (e.g., in a loop), remove GoBackground().
# hg.Program.GoBackground()

The screenshot below shows the "Enable detailed logging for this switch." checkbox added to the Settings dialog of a Switch module.

Once a feature is added, your program can check its status or react to changes in several ways:

1. Reacting to feature changes via events:

You can use hg.When.ModuleParameterChanged to be notified whenever any module parameter changes. Inside the event handler function, you can specifically check if the changed parameter (par) is your feature's field name or if the module triggering the event (mod) currently has the feature enabled.

# Example: Reacting when the feature's value changes or when any parameter
#          changes on a module that has the feature enabled.

def module_updated_fn(mod, par):
    # Option A: Check if the module reporting the change has our feature enabled.
    # Useful if you need to perform an action related to the module regardless
    # of *which* parameter changed, as long as the feature is active.
    if mod.HasFeature("MyFeatures.ExtraLogging.Enable"):
        # Example: Log extra details whenever *any* parameter of this module changes
        hg.Program.Log.Info(f"Extra Log for {mod.Instance.Name}: Param '{par.Name}' changed to '{par.Value}'")
        pass # Add specific logic here

    # Option B: Check if the specific parameter that changed *is* our feature field.
    # Useful for reacting only when the feature setting itself is toggled.
    if par.Is("MyFeatures.ExtraLogging.Enable"):
        hg.Program.Notify(f"Module '{mod.Instance.Name}' Extra Logging option changed to '{par.Value}'")
        # Add logic here for when the setting is enabled/disabled
        pass

    return True # Return True to keep the handler active, False to unregister

# Register the event handler (typically in Setup or initial Main block part)
hg.When.ModuleParameterChanged(module_updated_fn)

This event-driven approach is efficient as your code only runs when relevant changes occur.

2. Querying Modules with the feature enabled:

You can proactively select all modules that currently have a specific feature enabled (and set to a truthy value like "true" for checkboxes or non-empty for text) using hg.Modules.WithFeature(...). This returns a ModulesManager instance representing the selected modules.

You can then iterate over these selected modules using the .Each() method, providing a callback function that will be executed for each module found.

# Example: Iterating over all modules with the logging feature enabled

# Get a ModulesManager instance for modules with the feature active
enabled_modules = hg.Modules.WithFeature("MyFeatures.ExtraLogging.Enable")

# Define the function to execute for each selected module
def module_iterator(mod_helper):
    # 'mod_helper' is the ModuleHelper wrapper for the current module
    hg.Program.Log.Info(f"Module with Extra Logging enabled: {mod_helper.Instance.Name}")
    # Perform actions on this specific module if needed
    # mod_helper.On() # Example action
    return False # Return False to continue iterating through all matches

# Execute the iterator function for each module in the selection
enabled_modules.Each(module_iterator)

# To get the count of selected modules, access the SelectedModules list:
count = len(enabled_modules.SelectedModules)
hg.Program.Log.Info(f"Found {count} modules with Extra Logging enabled.")

The .Each() method is convenient for applying an action to all matching modules. Returning True from the iterator function will stop the iteration prematurely.

3. Accessing the raw list of selected modules:

The ModulesManager instance returned by hg.Modules.WithFeature(...) (and other selection methods) also provides direct access to the list of underlying Module objects via the .SelectedModules property. This gives you a standard Python list that you can iterate over using a regular for loop. Note that this list contains the raw Module instances, not the ModuleHelper wrappers used in .Each() or event handlers.

# Example: Accessing the raw list of Module instances

enabled_modules = hg.Modules.WithFeature("MyFeatures.ExtraLogging.Enable")

# Get the list of selected Module objects
selected_list = enabled_modules.SelectedModules

hg.Program.Log.Info(f"Iterating through {len(selected_list)} selected module instances:")
for m_instance in selected_list:
  # m_instance is the raw Module object
  hg.Program.Log.Trace(f"- Domain: {m_instance.Domain}, Address: {m_instance.Address}, Name: {m_instance.Name}")
  # Access other properties of the Module instance directly

Adding program options

While AddFeature adds settings to individual modules, hg.Program.AddOption adds configuration settings that apply to the program as a whole. This is the correct method for settings like API keys, file paths, or global timing parameters like the polling interval for our sensor example.

Options added via hg.Program.AddOption appear in the main System -> Settings page, grouped under a section named after the program, using appropriate UI controls based on the specified type.

The program code can then retrieve the value set by the user using hg.Program.Option(...) and adjust its behavior. Let's make the polling interval from our previous sensor example configurable using a slider:

# Example: Add a program option to control the sensor polling interval using a slider

# Typically placed at the start of the Main block or in Setup if using hg.Program.Run()

option_name = "Settings.PollingIntervalSeconds"
option_description = "Interval in seconds between sensor readings (0.5-60)."
default_interval = 1 # Default value if not set or invalid

hg.Program.AddOption(
    option_name,           # Parameter 1: field (name)
    str(default_interval), # Parameter 2: defaultValue (converted to string)
    option_description,    # Parameter 3: description
    "slider:0.5:60:0.5"    # Parameter 4: type
)

# If the program only registers the option, use GoBackground.
# If it runs the sensor loop that uses the option, remove GoBackground().
# hg.Program.GoBackground()

The screenshot above shows the "Interval in seconds between sensor readings." numeric input field within the program's section on the "System -> Settings page"

Reading program options

Once a program option is registered using hg.Program.AddOption, its current value (as set by the user in System Settings, or the default value if unchanged) can be retrieved within the program's code. This allows the program to adapt its behavior based on user configuration.

The API method hg.Program.Option is used for this, passing the unique option_name. This method returns a ModuleParameter object containing the option's details. To get the numeric value directly, you should use the .DecimalValue property of this object. This property returns the value as a double (equivalent to Python's float), handling the necessary conversion internally.

# Example: Reading the polling interval value registered earlier
option_name = "Settings.PollingIntervalSeconds"
default_interval = 1.0 # Define default as float

# Attempt to retrieve the ModuleParameter object for the option
option_param = hg.Program.Option(option_name)

polling_interval = default_interval # Start with the default

# Check if the option parameter object was found
if option_param:
    # Access the value directly as a double/float using .DecimalValue
    try:
        polling_interval = option_param.DecimalValue
        # Optional extra validation based on context/range
        if not (0.5 <= polling_interval <= 60):
             hg.Program.Log.Warn(f"Value {polling_interval} for {option_name} outside expected range (0.5-60). Using default.")
             polling_interval = default_interval
    except Exception as e:
        # Handle potential errors if the value couldn't be represented as double
        hg.Program.Log.Error(f"Error getting DecimalValue for option '{option_name}': {e}. Using default.")
        polling_interval = default_interval
else:
    # The option wasn't found (e.g., typo in name)
    hg.Program.Log.Warn(f"Option '{option_name}' not found. Using default value.")
    # polling_interval already holds the default

# Now 'polling_interval' holds the configured value (as float) or the default
# hg.Program.Log.Trace(f"Using polling interval: {polling_interval}s")
# hg.Pause(polling_interval)

Using the .DecimalValue property is the recommended way to retrieve numeric option values as it simplifies the code by avoiding manual string parsing and conversion. For non-numeric types, you would typically access the .Value (string) property, or potentially other specific properties if available for types like booleans.

Using .NET libraries in Python

Thanks to the IronPython engine, your Python programs in HomeGenie are not limited to standard Python libraries. You can directly access and utilize the vast ecosystem of .NET libraries, including the standard .NET Framework / .NET Core libraries and the same custom or third-party libraries (like NWaves, YoloSharp, LLamaSharp, etc.) available to C# programs.

This is achieved using the clr (Common Language Runtime) module built into IronPython:

  1. Import clr: Start your script with import clr.
  2. Add Reference: Use clr.AddReference("AssemblyName") to load the desired .NET assembly. For standard .NET libraries (like System, System.IO), you typically use the assembly name without the .dll extension. For custom libraries included with HomeGenie or placed in specific folders, you might use clr.AddReferenceToFileAndPath("path/to/your.dll").
  3. Import Namespaces/Types: Once the reference is added, you can import namespaces and types from the .NET library using standard Python import syntax (e.g., from System.IO import File).
  4. Use .NET Types: Instantiate and use the .NET classes and methods directly within your Python code, leveraging Python's syntax.
# Example: Using the .NET System.IO library to check if a file exists

import clr

try:
  # Load the System assembly (contains basic types like String) - often loaded by default
  # clr.AddReference("System") # Usually not needed but good practice if unsure

  # Load the assembly containing file system operations
  clr.AddReference("System.IO")
  # Import specific classes from the System.IO namespace
  from System.IO import File, Path

  # Now you can use the .NET classes
  file_path = "/path/to/your/config.txt"
  if File.Exists(file_path):
    hg.Program.Log.Info(".NET Check: File exists at " + file_path)
    # You could also read the file using System.IO.File methods here
  else:
    hg.Program.Log.Warn(".NET Check: File not found at " + file_path)

  # Example of using Path class
  directory_name = Path.GetDirectoryName(file_path)
  hg.Program.Log.Info("Directory part: " + directory_name)

except Exception as e:
  hg.Program.Log.Error("Error using .NET libraries: " + str(e))

This capability significantly extends the power of Python scripting within HomeGenie, allowing you to leverage existing .NET code and libraries seamlessly alongside standard Python modules.

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