Kotlin alternative to AppDaemon, an automation environment and library for Home Assistant.
Do you like Home Assistant and want to take automation to the next level? Do you want to create an Android application that needs to communicate with a Home Assistant instance? Have you tried AppDaemon but you found the typeless programming in Python to be a giant trial-and-error situation? Well, then KHomeAssistant is the library / add-on for you!
Using an IDE like Intellij and the great Kotlin language you can easily verify whether your code will run and behave before you use it with Home Assistant. Plus, if you are familiar with Java or other object based programming languages, you'll quickly feel at home with Kotlin. The autocomplete is your friend and the type completion is godsent, so there's much to enjoy.
At the moment the project reached a bit of a roadblock regarding coroutines and the (wrong) use of runBlocking inside getter/setters. Either suspend properties get added to Kotlin (https://youtrack.jetbrains.com/issue/KT-15555) or the way attributes are used needs to be redesigned. I'm keen on making it as easy as possible for the end user so of course I would prefer the former, however, the people at JetBrains haven't shown a lot of motivation to add that feature yet. We'll see....
Let's get a quick comparison between an AppDaemon automation and the KHomeAssistant variant, as well as an abbreviated version.
class OutsideLights(hass.Hass):
off_scene = "scene.outside_lights_off"
on_scene = "scene.outside_lights_on"
def initialize(self):
self.run_at_sunrise(self.on_sunrise, offset=900)
self.run_at_sunset(self.on_sunset, offset=-900)
def on_sunrise(self, kwargs):
self.log("OutsideLights: Sunrise Triggered")
self.turn_on(self.off_scene)
def on_sunset(self, kwargs):
self.log("OutsideLights: Sunset Triggered")
self.turn_on(self.on_scene)
And now the same automation but converted directly to Kotlin:
class OutsideLights(kHass: KHomeAssistant) : Automation(kHass) {
private val offScene: Scene.Entity = Scene.Entity("outside_lights_off")
private val onScene: Scene.Entity = Scene.Entity("outside_lights_on")
override suspend fun initialize() {
runEveryDayAtSunrise(offset = 15.minutes, callback = ::onSunrise)
runEveryDayAtSunset(offset = -15.minutes, callback = ::onSunset)
}
private suspend fun onSunrise() {
println("OutsideLights: Sunrise Triggered")
offScene.turnOn()
}
private suspend fun onSunset() {
println("OutsideLights: Sunset Triggered")
onScene.turnOn()
}
}
This looks very similar right? As a bonus offScene
is now of the type Scene.Entity
meaning we can now easily see that onScene.entities
will give us a list of entities that are affected by this scene, thanks to the autocomplete of the IDE.
We can also private our functions and attributes to keep the project clean as well as use typed TimeSpans thanks to the klock library.
Now let's Kotlin-ify, because no one wants to type stuff that isn't necessary.
class OutsideLights(kHass: KHomeAssistant) : Automation(kHass) {
private val offScene: Scene.Entity = Scene["outside_lights_off"]
private val onScene: Scene.Entity = Scene["outside_lights_on"]
override suspend fun initialize() {
runEveryDayAtSunrise(offset = 15.minutes) {
println("OutsideLights: Sunrise Triggered")
offScene.turnOn()
}
runEveryDayAtSunset(offset = -15.minutes) {
println("OutsideLights: Sunset Triggered")
onScene.turnOn()
}
}
}
As you can see, callbacks can be inlined, making it much clearer what the automation does when.
Using helper functions in the library .Entity()
can be replaced with []
which is less typing and still quite clear.
Also note that type annotations like : Scene.Entity
can be omitted as the IDE will hint the type anyways.
While calling services directly from an automation is still possible,
callService(serviceDomain = "light", serviceName = "turn_on", entityID = "light.bedroom_lamp")
this is far from ideal to work with. Hence, the library 'objectifies' domains and entities so that service executions can be done like calling a function on an object. This converts the service call from above to
Light.Entity("bedroom_lamp").turnOn()
\\ or
Light["bedroom_lamp"].turnOn()
Services that do not take an entity ID as input can be called from the domain itself. For example:
callService(serviceDomain = "homeassistant", serviceName = "restart")
becomes simply
HomeAssistant.restart()
Now you can see that because domains and entities are now typed, there can be a lack of support in the library. While it is encouraged to help expand the library, in the meantime you can use something like this:
Domain("some_unsupported_domain")["some_entity"]
.callService(serviceName = "some_service", data = buildJsonObject { put("some_value", 10) })
States are an important part of Home Assistant entities and are usually represented as a String. However, sometimes these states are actually Floats like for an "input_number" or the state can only be one of several types, like "on" or "off" for a "light" entity. Hence, states in KHomeAssistant are typed and dependent on the type of the entity. For example:
val lightState: OnOff = Light["some_light"].state
when (lightState) {
ON -> { /* do something */ }
OFF -> { /* do something else */ }
UNKNOWN, UNAVAILABLE -> { /* notify or something */ }
}
As you can see the state of a Light
is represented as the enum class OnOff
which can only exist in a certain amount of states.
In some cases states are also writable, which is dependent on the domain's implementation.
For instance, Light
performs a turnOn()
when the state
is set to ON
and a turnOff()
when it's set to OFF
.
The IDE will tell you if it's possible to set the state like that or not.
Most entities in Home Assistant have their own attributes. This can range from the current brightness of a light or the volume level of the media player. Unlike AppDaemon, attributes are available directly as properties of an entity. This means you can get the volume of your stereo like:
val volume: Float = MediaPlayer["stereo"].volume_level
(If an entity does not have the attribute an exception will be thrown unless a default is set in the attrsDelegate
in the entity class.
This means that if you're not sure your entity actually has the property you want, you must surround it with try { ... } catch (e: Exception) { ... }
).
Aside from this, KHomeAssistant also allows attributes to be writable. This means that
MediaPlayer["stereo"].volume_level = 0.3f
will under the hood call
MediaPlayer["stereo"].volumeSet(0.3f)
This allows for beautiful notations like:
MediaPlayer["stereo"].volume_level += 0.05f
Attributes in KHomeAssistant are cached and updated when state changes occur, meaning they should always be up to date. If the cache has not been updated in a while it will fully refresh it.
If you want to take a look at the current state / attributes of an entity, simply print it.
The toString
method of an entity is very powerful.
An important part of automations is being able to react to state- or attribute changes. Most entities include their own helper functions to provide listeners which improves readability and understandability. For instance, there's:
Light["bedroom_lamp"].onTurnedOn {
// do something
}
or
GarageDoorBinarySensor["garage_door"].onOpened {
// do something
}
However, not all possible attributes and states can be covered with helper functions, so KHomeAssistant provides a few extension functions on all entities. For example, for state changes there is:
MediaPlayer["stereo"].onStateChangedTo(PAUSED) {
// do something
}
but also a more general
myEntity.onStateChanged {
// do something
}
and even
myEntity.onStateChanged({ oldValue, newValue ->
// do something with old or new state value
})
Listening for attribute changes works in a similar manner. You can listen for any attribute change using
myEntity.onAttributesChanged {
// do something
}
or you can specify which attribute to listen for. This can be any property or attribute of the entity of your choice. For instance
val stereo = MediaPlayer["stereo"]
stereo.onAttributeChangedNotTo(stereo::source, "TV") {
// turn off the tv or something
}
// or in DSL style (for more info see below)
MediaPlayer["stereo"] {
onAttributeChangedNotTo(::source, "TV") {
// turn off the tv or something
}
}
For all callbacks, this
corresponds to the entity. This means you can simply type
Switch["bedroom_switch"].onTurnedOn {
// toggle the lights or something
// turns off the switch
turnOff()
}
Scheduling when to run something is another essential part for automation. While you can freely
use delay(5.seconds)
in your code (as the thread will then simply suspend for 5 seconds), if
you want to schedule something for each day, this is undoable.
KHomeAssistant uses an in-house built scheduler to make this easy.
For regular time intervals, there are functions available like
runEveryMinute {
// this gets run at the start of each minute
}
and
runEveryHour(offset = 30.minutes + 15.seconds) {
// this gets run every hour at the 30 minute mark plus 15 seconds
}
You can even go completely custom
runEvery(
timeSpan = 1.9.hours + 23.minutes - 4.8.seconds + 1.milliseconds,
alignWith = DateTime.nowLocal()
) {
// do something
}
There are also irregular intervals such as running something at sunset. Offsets are also available.
runEveryDayAtSunset(offset = -15.minutes) {
// this gets run every day 15 minutes before sunset
}
As the time of the sunset changes every day, the next execution time gets updated automatically
using an attribute listener.
You can also create a scheduled execution for a changing execution time yourself (for instance
using a datetime_input
). To understand how that would work, let's look again at how runEveryDayAtSunset
works.
runAt(
getNextLocalExecutionTime = { sun.nextRising }, // define how to get the execution time value
whenToUpdate = { update -> // define when to update the execution time value
sun.onAttributeChanged(sun::nextRising) { update() } // namely when the nextRising attribute changes
}
) {
// do something
}
Finally, you can define one-off schedulings.
There is runIn()
where you can schedule something to run in a certain amount of time from now.
For example:
runIn(5.minutes) {
// do something
}
There's also runAt()
where you can define when to run something at a certain point in (local) time.
For example:
runAt(
DateTime(
year = Year(2020),
month = Month.September,
day = 22,
hour = 13,
minute = 30
).localUnadjusted
) {
// do something
}
All schedules return a Task
instance, which can be cancel()
'ed at any time.
I'm still working on getting KHomeAssistant to work as an Add-On for Home Assistant, however, in the meantime, you can already test KHomeAssistant from your own PC, as long as you can connect to your Home Assistant instance over the network / internet. Recommended tools are Intellij by Jetbrains and some Kotlin knowledge of course.
The library is not yet published, so for now, you can keep the library (KHomeAssistantLibrary) and your instance consisting of automation (KHomeAssistantExample) in the same project. Eventually, the library will be published.
Firstly you'll need a Long Lived Access Token. This, you can create by going to your profile on the web interface, scrolling down and creating a new one.
All communication between the program and the Home Assistant instance goes via KHomeAssistantInstance
.
To get started, create an instance like
val kHomeAssistant = KHomeAssistantInstance(
host = "THE IP OR HOSTNAME OF YOUR INSTANCE",
port = 8123, // for instance
secure = true, // true if you're using https instead of http
debug = false, // prints more messages if true
timeout = 2.seconds, // Timeout for confirmation for updating states and attributes.
accessToken = "THE ACCESS TOKEN"
)
Then, using this instance, you can call .run
on it from a Coroutine context with instances of the automations
you want to run. This can be done from the main()
method if you like. For instance:
fun main() {
runBlocking {
kHomeAssistant.run(OutsideLights(kHomeAssistant)/*, maybe others */)
}
}
And that's all!
The beauty of Kotlin is that you can use it however you like and if you want, you can combine the strengths of object oriented programming and functional programming.
To start off, all automations can also be created using a function call instead of having to create a class.
val automation: Automation = automation(kHomeAssistant, "Automation Name") {
// this can be seen as the initialize method
}
Automations can even be defined directly in the run
method if you would like to do so.
kHomeAssistant.run(
automation("one") {
// one automation
},
automation("other") {
// other automation
}
)
Next, all entities are invokable, DSL-style, which can be compared to calling .apply { }
on it.
This makes for very readable and clear syntax and less typing.
No one is a fan of syntax like
bedroom_lamp.color = Colors.RED
bedroom_lamp.brightness_pct = 100f
bedroom_lamp.onAttributeChanged(bedroom_lamp::effect) {
// do something
}
instead, you can use
bedroom_lamp {
color = Colors.RED
brightness_pct = 100f
onAttributeChanged(::effect) {
// do something
}
}
which does exactly the same thing, however it is a lot clearer with less typing (so less room for error).
If you want to target multiple lights at once, simply put them in a list or array and go for it:
val lights = listOf(bedroom_lamp, other_lamp)
lights {
color = Colors.RED
brightness_pct = 100f
onAttributeChanged(::effect) {
// do something
}
}
This saves you from even having to write a for loop.
If all you ever want is to address multiple entities at once and don't want to address them individually, you can immediately create a list of entities using:
val lights: List<Light.Entity> = Light.Entities("light1", "light2", "light3")
// or
val lights: List<Light.Entity> = Light["light1", "light2", "light3"]
This also means that
val light1 = Light["light1"]
val light2 = Light["light2"]
val light3 = Light["light3"]
can (only inside a function) be shortened to:
val (light1, light2, light3) = Light["light1", "light2", "light3"]
Another fun notation is the delegate notation. You can initialize an entity like this:
val bedroom_lamp by Light
For this to work, the name of the variable needs to exactly match the name of the entity in Home Assistant.
- switch
- sun
- scene
- notify
- mqtt
- media_player
- light
- home_assistant
- hassio
- group
- input
- input_boolean
- input_datetime
- input_number
- input_select
- input_text
- binary_sensor (split per device_class)
- generic
- battery
- connectivity
- ...
- all of them are present
- sensor (split per device_class)
- generic
- battery
- TODO
- weather
- cover
- air_quality
- alarm_control_panel
- fan
- remote
- Tell me what TODO next!
- You can also implement a domain yourself following the
ExampleDomain
in the KHomeAssistantExample subproject.
This library is VERY MUCH a work in progress. If you're feeling adventurous you can try it out or help by creating implementing more domains! Any tips or contributions are welcome as well.
Testing is a large part that still needs to be done.
While the library uses Kotlin Multiplatform. Currently only Kotlin/JVM works. There are still some reflection pieces missing in Kotlin/JS which I hope will be added soon. Kotlin/Native, same story.