As a home automation set up grows in size and complexity, there are certain issues that manifest themselves. The problem of lighting has been discussed on this blog in various places; not only because it serves as a great example but also because it is a surprisingly complex problem to solve from a software architecture and implementation point of view. Many of it’s intricacies can be applied to other aspects of home automations beyond the scope of lighting.
My latest attempt at solving it saw the use of AppDaemon to implement virtual
MotionLight devices as Python Objects. I highly recommend you read that article, particularly the numbered requirements I identified because those are critical for a lighting system to be stable, usable, reusable and independent.
- Lights should be triggered automatically and reliably. You want to be able to walk into a room and have the light come on at the same time. This should work seemlessly to the point you forget it was ever a problem to begin with.
- There should be a negligible delay between trigger (e.g. motion) and light turning on. If an automated lighting system causes more frustration than a switch on the wall then it is not usable.
- We should aim to keep the information sent into the HA system (sensor data) separate from the information coming out (automation). It makes no sense to keep a motion
onstate for 5 minutes in order to turn on a light for the same duration. This creates tight coupling and makes the sensor less reusable because the sensor itself makes an assumptions about how its state is going to be used.
- The automation should work independently from other automations. If interrupted or otherwise impacted by other automations, lighting should take the appropriate action to exit the scenario gracefully. For example, a motion light turns on. The user decides to watch a movie; this triggers a home cinema lighting automation, dimming the room. We do not want the motion light to turn off the living room lighting because the problem shifted from “make light so I can see” to “provide pretty home cinema lighting”.1 Similarly, it would be bad design to add a check to the motion light to make sure that the TV is off before turning off a light. This creates coupling between automations which is unmaintainable and difficult to plan for.
To summarise briefly, the objective is to eliminate the interference caused by different automation scripts controlling the same
light entity on the home automation server. The AppDeamon implementation (though still a work in progress) is an acceptible solution to encapsulate motion controlled lights and their trigger into one entity. This entity is solely responsible to fulfil the listed requirements.
What we want is a more general approach, one that can be applied—not just to
MotionLights—but any call to a
light-service in Home Assistant. A solid implementation will allow this approach to be applied to any service.
In software architecure, we typically use locks to control access to a resource. This ensures a resource is accessed by one consumer at a time. There is also the notion of priorities, where, for example, higher priority CPU threads receive more CPU time, or packets queued on a high priority Ethernet link are processed first on the network router.
We can combine these concepts into—what I now like to call—a priority lock.
Let us walk through some scenarios to understand how this concept and the underlying mechanism should work. After that (most likely in a follow up post) we can explore how to implement such a lock in software.
How to aquire a lock
When controlling lighting in a simple on/off fashion, automations must aquire a priority lock when making the service call.
Passing this priority lock around enables the system to give priority to some control sources whilst ignoring the input from others.
If, during the automation execution, another entity with higher priority calls a
light-service then the lock should be given to the higher priority entity, preventing the original automation from changing the light any further.
Concrete Example: A
MotionLight is turned on because you enter a room. You (or some other automation) changes the light color to yellow and reduce the brightness a bit. Since the lock has been transferred to the higher priority manual control, the
MotionLight cannot make further changes to the light (because it is unable to aquire a lock on it due to its low priority). This relates to the independent property.
You can understand how this would prevent different systems affecting each other and overriding manual lighting1.
The following diagram illustrates the sequence of events (vertical top to bottom is time):
Sequence Diagram showing a scenario where a lock is passed to different entities.
Priority: integer 1-10 (1 low, 10 high)
Manual control: 10 (supersedes all automations)
Motion lighting: 1 (lowest automation level)
Bedtime automation: 5 (somewhere inbetween, given it is the final thing executed in a day, a higher priority wouldn’t hurt. It is short running as well, meaning it will release any locks once done.)
It’s up to the service call to specify the priority. If none is supplied and the entity is unlocked, then a lock is briefly requested and then released (i.e. one off instant action). This is not possible when the entity is locked.
How locks are released
Locks are released when an automation is completed or when another automation with higher priority acquires the lock.
Scenario: Automation makes service call without a pre-existing lock A priority should be specified in the service call.
- If priority is lower than current lock, then request for control is ignored.
- If priority is higher, then control is granted to that automation2
- If no priority is specified then the service call will only be executed if no lock is on the
lightentity. (i.e. equivalent to a numeric value of 0)
Scenario: What’s to stop a priority 10 lock from locking the service indefinitely? A P10 lock can be passed only to other P10 automations. A light turned on via the web interface (P10) can be switched off using the web interface or wall RF panel (whose automations acquire a P10 lock). Both of these modes of operation are manual in nature, so it makes sense to assign them both the same priority value of 10.
It is up to the programmer to ensure similar modes of operation carry the same priority to enable flexiblity in releasing locks. In addition to this, locks should be released when the automation completes (automations typically do not run for long).
This post discussed some use cases of priority locks and they can solve some problems that exist in complex automation systems where automations interfere with one another. This is only required in large systems. If checking for some device status (e.g. TV) before turning off living room light works for you, then by all means go for it. In my experience this became unmaintainable and it is why I’m trying to come up with a generic and dynamic approach.
I use manual control as a mode that has the highest priority. If a light was controlled manually, then there is a good reason for it that Home Assistant can never truly understand. (You might have people over for dinner or whatever). It would be very annoying and awkward to have Home Assistant undo something you turned on manually. In most cases you think of “manual control” as a synonym for “an automation with high precedence”. ↩︎ ↩︎2
What happens to low priority automation? It was in the middle of accomplishing some task when the lock was taken away. Do we let it finish as if nothing happened or is it terminated with the loss of the lock? There may be a need for some termination callback to allow an automation to clean itself up before the lock is taken away. ↩︎