
Open media
Alongside visual programming, JavaScript, and Python, HomeGenie also offers C# as a powerful and versatile language for creating automation programs.
C# programs in HomeGenie differ from interpreted scripts like JavaScript and Python because they are compiled into optimized .NET assemblies (.dll files) which are then dynamically loaded as plugins, offering advantages such as enhanced performance, full access to the .NET framework, strong typing, and a dedicated "Context" code block for defining shared elements like helper classes or methods.
Let's explore some common API usage patterns within C# programs.
In HomeGenie, a module represents any controllable entity – a physical device, a virtual sensor, a service, etc. Programs can dynamically create new modules using Program.AddModule
to expose information or control points to the rest of the system, making them visible in the UI and available to other automations.
The following snippet shows how to add a module representing a DHT sensor connected via GPIO.
// Typically placed in the Setup code block
// Assign an arbitrary domain/address to identify this device
var domain = "Components.DHT22";
var address = "23"; // Example: the GPIO number connected to DHT data pin
// Add a new module of type "Sensor"
Program.AddModule(domain, address, "Sensor");
// Get a helper object (ModuleHelper) to interact with the module later
var dhtModule = Modules
.InDomain(domain)
.WithAddress(address).Get();
To signal significant information from a module, like a sensor reading, the module.Emit(...)
method is used on the ModuleHelper
instance obtained previously. This publishes the data (parameter name and value) to the system.
Emitted data is captured by HomeGenie for logging and visualization (e.g., in the Data Monitor). It also triggers events that other programs can react to using When.ModuleParameterChanged
, enabling complex interactions and data processing pipelines.
// Typically placed in the Main code block, often within a loop or event handler
// [...] code to read humidity and temperature values from the sensor omitted
// Ensure dhtModule was successfully retrieved in Setup and is accessible
if (dhtModule != null)
{
// Emit sensor values using standard parameter names
dhtModule
.Emit("Sensor.Humidity", humidity.Percent)
.Emit("Sensor.Temperature", temperature.DegreesCelsius);
}
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.
You can expose custom endpoints for your C# program using Api.Handle
. This allows external systems or other HomeGenie components to trigger actions via HTTP requests. You provide a path prefix and a lambda expression (or method group) that takes the raw request string
data and returns a ResponseStatus
object. Use Api.Parse
inside the handler to easily access request details.
// Typically placed in the Setup code block
Api.Handle("My.Domain", (requestData) => {
// Parse the raw string data into a structured request object
var request = Api.Parse(requestData);
// Access request details: request.Domain, request.Address, request.Command
// request.Data (for POST), request.GetOption(index), etc.
// <domain>/<address>/<command>/<option_1>/.../<option_n>
// in this case <domain> is always "My.Domain"
if (request.Command == "ping")
{
// While ResponseStatus is standard, simple string returns *might*
// still work for basic cases, but ResponseStatus is preferred.
// Let's assume PONG is custom and return it directly.
return "PONG from C#";
}
if (request.Command == "Control.On")
{
// TODO: Add C# code to turn ON the device/service
// Return standard OK status
return new ResponseStatus(Status.Ok);
}
if (request.Command == "Control.Off")
{
// TODO: Add C# code to turn OFF the device/service
return new ResponseStatus(Status.Ok);
}
// ... handle other commands ...
// Return standard ERROR status with a message for unknown commands
return new ResponseStatus(Status.Error, $"Unknown API command: {request.Command}");
});
// Use GoBackground if the program only registers handlers in Setup
// and doesn't need an active Main loop.
// Program.GoBackground();
Custom API commands registered this way can also be conveniently called from visual programs using the "Control->Custom Command" block.
Just as programs can expose APIs, they can also call APIs provided by HomeGenie itself or other programs and modules using the Api.Call
static method. This is the primary mechanism for inter-program communication and controlling devices or services managed by other parts of the system.
You provide the target API path (the part after /api/
, typically in the format <domain>/<address>/<command>
) and optionally, an object containing data to be sent (often used for POST-like requests or passing complex parameters).
// Example: Turning on a light module with domain "My.Domain" and address "Lamp1"
try
{
// Target path format: <domain>/<address>/<command>
var response = Api.Call("My.Domain/Lamp1/Control.On");
// The response is typically a ResponseStatus object or a simple string
Program.Log.Info($"API Call Response: Status={response?.Status}, Message='{response?.Message}'");
}
catch (Exception e)
{
Program.Log.Error($"Error calling API: {e.Message}");
}
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 using method invocation. This is substantially faster. Furthermore, if the called API handler (registered via Api.Handle
) is also running within the same HomeGenie instance and its lambda signature is designed to accept specific C# types (beyond the basic string
requestData), Api.Call
can pass complex objects (like lists, dictionaries, or custom class instances) directly without needing JSON serialization/deserialization. The receiving handler gets a reference to the original 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 use Program.AddFeature
to dynamically add settings to the Settings dialog of specific module types. This enables per-module configuration of specialized behavior, allowing users to enable or customize features on individual devices like lights or sensors.
// Typically placed in the Setup code block
Program.AddFeature(
"", // Domain filter (empty = all domains)
"Light,Dimmer,Color", // Type filter (only apply to Lights)
"MyFeatures.NightMode.Enable", // Unique feature field name
"Enable automatic dimming at night.", // Description for user
"checkbox" // UI control type
);
// If only registering features in Setup, consider using GoBackground()
// Program.GoBackground();
The screenshot below shows the "Enable automatic dimming at night." checkbox added to the Settings dialog of a light module.
Once a feature is added, your C# program can check its status or react to changes in several ways:
1. Reacting to feature changes via events:
You can use When.ModuleParameterChanged
to subscribe to parameter changes. Inside the event handler (a lambda expression or method), you can check if the triggering module (module
) has the feature enabled using module.HasFeature()
or if the changed parameter (parameter
) specifically matches your feature's field name using parameter.Is()
.
// Example: Reacting when the feature's value changes or when any parameter
// changes on a module that has the feature enabled.
// Register the event handler (typically in Setup)
When.ModuleParameterChanged((module, parameter) => {
// Option A: Check if the module reporting the change has our feature enabled.
if (module.HasFeature("MyFeatures.NightMode.Enable"))
{
// Example: Perform action when *any* parameter changes on a Night Mode enabled light
Program.Log.Info($"Night Mode Light '{module.Instance.Name}' changed param '{parameter.Name}' to '{parameter.Value}'");
// TODO: Add specific logic here
}
// Option B: Check if the specific parameter that changed *is* our feature field.
if (parameter.Is("MyFeatures.NightMode.Enable"))
{
Program.Notify($"Module '{module.Instance.Name}' Night Mode option changed to '{parameter.Value}'");
// Add logic here for when the setting is enabled/disabled
// TODO: Add specific logic here
}
// Note: In C#, the lambda implicitly stays registered unless explicitly removed.
});
This event-driven approach allows efficient reaction to state changes.
2. Querying modules with the feature enabled:
Use Modules.WithFeature(...)
to get a ModulesManager
instance representing all modules where the specified feature is currently enabled (i.e., has a truthy value).
You can then iterate over these selected modules using the .Each()
method. This method expects a lambda expression or method group matching the signature Func<ModuleHelper, bool>
. This function is executed for each matching module helper. Returning false
from the function continues the iteration, while returning true
stops the iteration immediately.
// Example: Iterating over all modules with the Night Mode feature enabled
// Get a ModulesManager instance for modules with the feature active
var enabledModules = Modules.WithFeature("MyFeatures.NightMode.Enable");
// Execute an action for each selected module helper using a Func<ModuleHelper, bool>
enabledModules.Each(moduleHelper => {
// 'moduleHelper' is the ModuleHelper wrapper for the current module
Program.Log.Info($"Module with Night Mode enabled: {moduleHelper.Instance.Name}");
// Perform actions on this specific module if needed
// moduleHelper.SetLevel(10); // Example: Dim the light
// Return false to ensure the iteration continues for all modules
return false;
});
// To get the count of selected modules, access the SelectedModules list's Count property:
int count = enabledModules.SelectedModules.Count;
Program.Log.Info($"Found {count} modules with Night Mode enabled.");
The .Each()
method is convenient for applying an action concisely. Remember to return false
from your lambda if you want to process all matching modules, or true
if you need to stop early based on some condition.
3. Accessing the raw list of selected modules:
The ModulesManager
instance also provides direct access to the underlying List<Module>
via the .SelectedModules
property. You can iterate over this list using a standard foreach
loop to access the raw Module
instances directly.
// Example: Accessing the raw list of Module instances
var enabledModules = Modules.WithFeature("MyFeatures.NightMode.Enable");
// Get the List<Module> of selected Module objects
var selectedList = enabledModules.SelectedModules;
Program.Log.Info($"Iterating through {selectedList.Count} selected module instances:");
foreach (var mInstance in selectedList)
{
// mInstance is the raw Module object
Program.Log.Trace($"- Domain: {mInstance.Domain}, Address: {mInstance.Address}, Name: {mInstance.Name}");
// Access other properties of the Module instance directly
}
To add global configuration settings for the program itself (not tied to individual modules), use the Program.AddOption
method. These settings appear in the main System -> Settings page under the program's section, providing a central place for users to configure parameters like API keys, file paths, or location information.
// Typically placed in the Setup code block
Program.AddOption(
"Yolo.ModelPath", // Unique option identifier name
Data.GetFolder() + "/yolo11n.onnx", // Default value
"Path of YOLO model file (.onnx)", // Description shown to user
"text" // UI field type (using a standard type)
);
// Example of reading the option later (e.g., in Main or Setup)
// string modelFilePath = Program.Option("Yolo.ModelPath").Value;
The screenshot below shows the "Path of YOLO model file" input field within the program's section on the System -> Settings page)
HomeGenie C# programs benefit from several pre-linked libraries, providing ready-to-use functionality for various advanced tasks without requiring manual setup. Below is a summary of the included libraries and their primary purpose:
NWaves is a comprehensive library for digital signal processing (DSP) focused on audio. It offers tools for generating various waveforms, audio encoding/decoding, filtering, synthesis, feature extraction (like MFCC), and various transformations. This makes it useful for both audio analysis and custom sound/signal generation tasks within HomeGenie.
homegenie / homegenie-base / sine-wave-signal
YoloSharp provides a .NET wrapper for the popular YOLO (You Only Look Once) family of real-time object detection models (specifically targeting ONNX Runtime). It simplifies integrating advanced computer vision capabilities like object detection, instance segmentation, and pose estimation into your automation programs, often used with camera feeds.
Object Detection - Realtime object detection using a YOLO model (ONNX)
package_2 homegenie / homegenie-ml-ai / object-detection
Instance Segmentation - Realtime instance segmentation using a YOLO model (ONNX)
package_2 homegenie / homegenie-ml-ai / instance-segmentation
Pose Estimation - Realtime pose estimation using a YOLO model (ONNX)
package_2 homegenie / homegenie-ml-ai / pose-estimation
LLamaSharp is a cross-platform .NET library enabling the integration and execution of Large Language Models (LLMs) based on the Llama architecture (and compatible models like Vicuna, Orca). It allows programs to leverage powerful generative AI capabilities for tasks like natural language understanding, text generation, or creating conversational interfaces.
ML.NET is Microsoft's open-source, cross-platform machine learning framework for .NET developers. It allows training, building, and deploying custom machine learning models directly within .NET applications. Common uses include sentiment analysis, price prediction, image classification, and anomaly detection, enabling data-driven intelligence in automation scenarios.
This collection of libraries facilitates interaction with low-level hardware protocols and sensors commonly used in IoT scenarios. It provides APIs for GPIO, I2C, SPI, PWM, and bindings for a wide range of sensors, displays, and other electronic components, making it easier to integrate custom hardware with HomeGenie running on devices like Raspberry Pi.
DHT Sensor - Adds a module for DHT-11/22 temperature and humidity sensor
package_2 homegenie / homegenie-iot-net / dht-xx-sensor
GPIO Pin - Adds a switch module to control a GPIO pin
package_2 homegenie / homegenie-iot-net / gpio-pin
Raspberry# is a community-driven library providing specific access to Raspberry Pi hardware features. While the Microsoft .NET IoT Libraries offer broader cross-platform support, Raspberry# includes helpers tailored explicitly for Pi hardware and may offer compatibility or specific functionality useful on older Raspberry Pi OS versions or for certain legacy applications.
DHT-11 Sensor - Adds a module for DHT-11 temperature and humidity sensor
package_2 homegenie / homegenie-iot-sharp / dht-11-sensor
DHT-22 Sensor - Adds a module for DHT-22 temperature and humidity sensor
package_2 homegenie / homegenie-iot-sharp / dht-22-sensor
GPIO Pin - Adds a switch module to control a GPIO pin
package_2 homegenie / homegenie-iot-sharp / gpio-pin
Grove Chainable RGB LED - Adds modules to control RGB LEDs
package_2 homegenie / homegenie-iot-sharp / grove-chainable-rgb-led