
Open media
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
).
Let's explore how to use the HomeGenie API within a Python program.
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());
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.
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
.
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.
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
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"
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.
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:
clr
: Start your script with import clr
.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")
.import
syntax (e.g., from System.IO import File
).# 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.