Skip to content

Automation Types

Leonard Sperry edited this page Jan 5, 2025 · 28 revisions

V11

There are two ways of creating automations. You can either use the IAutomationRegistry interface or write individual automations. This page covers the later of those two methods. When writing individual automations, simply create a class and implement one of the supported interface options:

Simple Auotmations

  • IAutomation
  • IAutomation<Tstate>
  • IAutomation<Tstate, Tatt>

Delayable Automations

  • IConditionalAutomation
  • IConditionalAutomation<Tstate>
  • IConditionalAutomation<Tstate, Tatt>
  • ISchedulableAutomation
  • ISchedulableAutomation<Tstate>
  • ISchedulableAutomation<Tstate, Tatt>

All of the above interfaces implement the IAutomationBase interface.

When implementing a generic automation, type conversion from the raw state change will be handled for you. See Generic Automations for more details.

IAutomationBase

public interface IAutomationBase
{
    EventTiming EventTimings { get => EventTiming.PostStartup; }
    IEnumerable<string> TriggerEntityIds();    
}

EventTimings

The EventTimings property tells the framework which events you care about. As you can see, it has a default implementation that only returns events after the application starts. However, at startup, the framework will read all messages from the topic which may or may not be compacted. This means that it will handle the latest entity state for all entities even if your application was not running when the events were created. If the topic has not compacted, it might handle older events. The EventTimings property will allow you to handle those events if you choose. For example your automation might have a line that looks like this:

public EventTiming EventTimings { get => 
    EventTiming.PreStartupSameAsLastCached | EventTiming.PreStartupPostLastCached | EventTiming.PostStartup; 
}

This would ensure that your automation would know the current state of an entity even if your application was down when the entity changed state. You may want to use this kind of option if, for example, you restarted your application at sunset, and you want to make sure lights get set appropriately.

See here for an example of how to consume pre-startup events.

TriggerEntityIds

All state changes for all entities with an ID that matches any of the ID's returned by this method will cause the Execute method of your automation to run, assuming the event timing matches as described above.

IAutomation

This is the primary interface used to trigger all calls into your automations. In fact, all automations end up being wrapped in an automation which implements this interface. In addition to the two members above, it exposes the Execute method.

You can add as many implementations of IAutomation as you need. All of them will be discovered at startup and will be registered as a singleton into the DI container. If you would like to have multiple instances of your automation, you must decorate your class with the ExcludeFromDiscoveryAttribute and register them via an IAutomationRegistry. See Tutorial for a detailed explanation.

public interface IAutomation
{
    Task Execute(HaEntityStateChange stateChange, CancellationToken cancellationToken);
}

Execute

Assuming the event timing and entity id of the state change matches your automation, the Execute method will be called with a HaEntityStateChange model and a CancelationToken. The state change object has 3 properties: EntityId, Old, and New. The Old and New properties represent the state of an entity at different points in time. The New property will always represent the current message being handled by the framework, and the Old will always represent what was previously stored in the cache. There is a possibility that Old will be null if it wasn't in the cache.

Note: The stateChange parameter does not have strongly typed properties for your entities. See State Extension Methods for ways of easily converting to strongly typed states.

IDelayableAutomation

Both the IConditionalAutomation and ISchedulableAutomation implement the IDelayableAutomation interface.

public interface IDelayableAutomation : IAutomationBase
{
    bool ShouldExecutePastEvents { get => false; }
    bool ShouldExecuteOnContinueError { get => false; }

    Task<bool> ContinuesToBeTrue(HaEntityStateChange haEntityStateChange, CancellationToken cancellationToken);
    Task Execute(CancellationToken cancellationToken);
}

All delayable automations are wrapped in an automation that implements the IAutomation interface which are in turn wrapped as discussed above. Instead of an Execute method, a delayable automation responds to state changes through the ContinuesToBeTrue method.

ContinuesToBeTrue

Internally, HaKafkaNet wraps your IConditionalAutomation as an IAutomation. Whereas in the IAutomation, the Execute method was called for all state changes, in the IConditionalAutomation the ContinuesToBeTrue method is called. If the method ever returns false, the delayed execution is canceled.

Execute

When the Execute method is called, it happens at some amount of time after a state change occurred and there could have been multiple state changes during that time. Therefore, there is no single state change that relates to the execution and no HaEntityStateChange object is passed in. After this method executes, your automation will be in a state as if ContinuesToBeTrue had returned false, meaning that the next time ContinuesToBeTrue returns true, your automation will be scheduled again.

ShouldExecutePastEvents

This method does not refer to pre-start up events. Where scheduling is concerned there could be times where the calculated schedule is before DateTime.Now. This property relates to that. Like in the example above regarding restarting your system at sunset. Perhaps your system was down before sunset, then sunset occurred, then your app started while handling pre-start up events, and your automation says to execute at sunset, which is now in the past. Set this to true if you want to handle that event.

ShouldExecuteOnContinueError

Unlike simple automations that fire immediately upon a state change, delayed automations execute at some scheduled time, and multiple state changes could have triggered. If one succeeds and schedules your automation to run, then another fails, what do you want to do? This property answers that question. 

IConditionalAutomation

The conditional automation is intended to ensure a state remains constant for a duration of time before it is executed. In addition to the members exposed by IDelayableAutomation it exposes one more.

public interface IConditionalAutomation
{
    TimeSpan For{ get; }
}

You interpret this interface as follows :

If condition ContinuesToBeTrue' for Foramount of time, thenExecute`.

For

This represents the amount of time to delay execution after the first time that ContinuesToBeTrue returns true.

Note, if ContinuesToBeTrue always returns true your automation would execute on an interval of the For time plus the time between state changes. If you do not want this behavior, it is recommended to recognize the state affected by Execute.

Note: if For returns TimeSpan.MinValue the automation will not run; the same as if ContinuesToBeTrue returned false.

See here for an example of using IConditionalAutomation

ISchedulableAutomation

Unlike the conditional automation that wants some state to exist for an amount of time before it triggers, the schedulable automation is intended for when information about scheduling exists in the state change itself. For example, when the sun.sun state changes, it tells you when the next sun rise/set/etc. will occur. You could also use the LastUpdatedDate property of an entity, which is used by the pre-built durable automations.

In addition to the IDelayableAutomation members, it exposes two more:

public interface ISchedulableAutomation : IDelayableAutomation
{
    bool IsReschedulable { get; }
    DateTime? GetNextScheduled();
}

IsReschedulable

When the sun entity reports state, its sunrise information won't change for roughly 24 hours. So, there is no need to reschedule and this property is false for all the sun based automations. However, if you created an automation based on a calendar, things would be different. Let's say an event on your calendar is scheduled in 4 hours. Your automation then schedules it, then another event is added to the calendar, but it is only 2 hours away. In that case, you would want to reschedule it by setting this to true.

GetNextScheduled

Unlike the conditional automation that uses a TimeSpan, the schedulable automation uses a DateTime. Upon a state change, if this automation is not already running or IsReschedulable is true, this method will be called after ContinuesToBeTrue is called.

Note: If this method returns null, the automation will not be scheduled; the same as if ContinuesToBeTrue returned false.

Scheduling is a complex topic. For more ideas on how you should schedule automations see Scheduling

Generic Automations

The generic versions of automations handle type conversion. For example, if you implemented IAutomation, you might have code that looks like this:

   public async Task Execute(HaEntityStateChange stateChange, CancellationToken ct)
    {
        var lightStateChange =  stateChange.ToOnOff<ColorLightModel>();
        // work with stronly typed properties
    }

Alternatively, you can implement IAutomation<OnOff, ColorLightModel> :

    public async Task Execute(HaEntityStateChange<HaEntityState<OnOff, ColorLightModel>> stateChange, CancellationToken ct)
    {
        // no conversion needed
        var brightness = stateChange.New.Attributes?.Brightness;
    });

In the fist example, you may have additional code to check that the type conversion succeeded or use a try/catch to account for breaking changes in either Home Assistant or an integration you have enabled. HaKafkaNet provides another interface for placing that type of code. the IFallbackExecution interface has one method that will be called if type conversion fails.

public interface IFallbackExecution
{
    Task FallbackExecute(Exception ex, HaEntityStateChange stateChange, CancellationToken ct);
}

Sometimes you only care about the state property and the attributes are of little importance. For example, you may be using a "Number" helper provided by Home Assistant to track some state. In that case you could implement IAutomation<float>. If you choose to only specify the type for the "state" property, the attributes will be typed as JsonElement.

Here is a more complete example

public class MyNumberTracker : IAutomation<float>, IFallbackExecution
{
    public async Task Execute(HaEntityStateChange<HaEntityState<float, JsonElement>> stateChange, CancellationToken ct)
    {
        float trackerValue = stateChange.New.State;
        // implement
    }

    public async Task FallbackExecute(Exception ex, HaEntityStateChange stateChange, CancellationToken ct)
    {
        // not likely to ever be called for a float
        // very useful for custom integrations
    }

    public IEnumerable<string> TriggerEntityIds()
    {
        yield return "input_number.my_number_helper";
    }
}
Clone this wiki locally