C#

Using C# in HomeGenie programs

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.

API usage examples

Let's explore some common API usage patterns within C# programs.

Adding modules

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();
Emitting data/values

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.

Adding a custom API handler

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.

Making an API call

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.

Adding features to modules

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
}

Adding program options

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)

Linked libraries documentation

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

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.

Examples

YoloSharp

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.

Examples

LLamaSharp

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

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.

.NET IoT libraries

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.

Examples

Raspberry#

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.

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