AppDaemon

Taking Home Assistant to the next level with Python — part 2

Marcel Blijleven
6 min readNov 8, 2021
Screenshot of Python code for an Appdaemon app

AppDaemon is a great way to take your smart home automations to the next level. In this series I will guide you through AppDaemon and how to use it to take your home automation to the next level.

  • Part 1: setting up AppDaemon and creating a simple light sequence
  • Part 2: turning off power to a sit-stand desk if it’s no longer in use after a certain time, and how to test your app (this article)
  • Part 3: controlling mechanical ventilation using HTTP requests and reporting its status back to Home Assistant

In this part I’ll show you an app that turns on and off a workstation and how you can use unit tests to verify correct behaviour. I will not go into unit testing or framework specifics too much and not all unit tests will be shown here.

Background

I have a sit-stand desk with a power strip which powers my laptop, monitor and a few other electronic devices. When not in use, the whole setup uses around 10 watts which isn’t that much but watts add up so it’s nice to reduce standby usage if possible. I had a working automation.yaml just for this, but there were some situations where it would trigger so I decided to move it to AppDaemon.

For this project I had these requirements:

  • Turn workstation on each working day at 07:45
  • Turn workstation off each working day at 18:00
  • Only turn off if the workstation is not in use (e.g. power usage ≤ 10 watts for 5 minutes)

I also wanted to unit test the custom methods I needed for this AppDaemon app. Why unit test? Because I want to make sure my code works, but also I want to be able to iterate on my apps without worrying about breaking things and not knowing about it.

With every automation, you always have to think: what will be the impact if it doesn’t work? — Paulus Schoutsen, founder of Home Assistant

Especially apps that control ‘critical’ automations can have a lot of impact when they stop working.

Project structure

This article will focus more on unit testing than actual AppDaemon functionality so I want to share my project structure with you, which will help you set up your project with unit tests too.

├── appdaemon
│ ├── apps
│ │ ├── __init__.py
│ │ ├── light_sequence.py
│ │ ├── ventilation.py
│ │ └── workstation.py
│ └── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── fixtures.py
│ ├── test_light_sequence.py
│ ├── test_ventilation.py
│ └── test_workstation.py
├── appdaemon.yaml
├── requirements.txt
└── requirements_dev.txt

Next to the default apps directory I’ve created a tests directory, inside there are a few files that are important for running the unit tests:

  • conftest.py, inside this file there is a line which configures the fixtures:pytest_plugins = “tests.fixtures"
  • fixtures.py, this file contains all the fixtures
  • test_*.py, these files contain all the unit tests. Inside these files I import the actual app code, e.g. inside test_ventilation.py I do:
    from apps.ventilation import Ventilation, VentilationClient.

There is also a requirements.txt and arequirements_dev.txt file. The former contains the appdaemon package requirement and the latter the requirements for testing (pytest and pytest_mock).

The app

As you can see, the initialize() method only calls other methods and splits the logic into smaller units. I consider it good practice to split your code into smaller chunks (units) of code. This will not only make it easier to read, it will also make it a lot easier to test.

I will go over each of these methods in the initialize() method separately and will some of the unit tests I’ve created. The unit tests are written using the packages pytest and pytest_mock. The main reason I like to use pytest is because of the easy fixtures.

Fixtures

Fixtures are reusable blocks of code that provide an object (or function) in a predetermined state. You can find the pytest documentation here. For my tests. Whenever you add a @pytest.fixture() decorator to a function, it will become a fixture. A fixture can return a value, instance of a class, a function etc.

The most important fixture is the workstation_fixture, which is basically an instance of Workstation with some mock values. Normally you wouldn’t instantiate your app class, but for the unit tests I had to. By providing some mock values, I managed to isolate my class under test from the default AppDaemon behaviour.

I’ve assigned a MagicMock to the cancel_timer, run_daily and run_every methods of the Workstation class. This disables the actual implementation of these method and allows me to verify if calls to these methods were made.

To mock entities from Home Assistant, I’ve created a simple MockEntity class which just has one field called state. The mock_entities fixture is a dict which has instances of the MockEntity as values.

Set entities

The method
The _set_entities() method reads the switch and sensor entity name from the app configuration and retrieves the actual entity using the entities dict provided by hassapi.Hass. If either the switch entity or the sensor entity is None, a ValueError is raised.

The tests
What _set_entities needs to do is assign the entity values to the Workstation instance and raise a ValueError if the entities are not found. In the first test I verify if the assignment is done correctly by first asserting both values are None before calling the _set_entities() method. After the method call, I verify if the values have been set to the expected (mock) values.

In the second test I use pytest's parametrize decorator to provide several parameters to a unit test. You can find more information about parametrize here, but what it basically does is iterate over the set of parameters and use those to populate the data inside then unit test. In the first set, I use the switch_entity name to set the value of the entity to None, in the second set I use the sensor_entity name to set the value of the entity to None. In both cases I verify that the raised ValueError contains the expected error text.

Set times

The method
The _set_times() method retrieves the startup and shutdown times from the Workstation args and applies them to the Workstation instance. To avoid duplicate code I’ve created a separate function for getting the times from the Workstation args.

The tests
The test_get_time_from_arg test is parametrized and checks for two different sets of data. Within this test I only assert correct arg values, in test_get_time_from_arg_raises I validate if the correct error is raised when the arg value is None or has missing keys.

The test__set_times test asserts that the startup_time and shutdown_time fields are properly set on the Workstation instance.

Set callbacks

The method
The _set_callbacks() method sets callbacks to run every workday to either turn the workstation on or off. It will check if startup_time and shutdown_time to prevent running with invalid data.

I’ve created two partials for the startup and shutdown. By creating partials, I can ‘freeze’ the function call with arguments and reuse it. So whenever I want to refactor this code, I only have to modify the function call in one place instead of multiples places.

For the shutdown run, I’ve created a custom constraint that checks if it is a weekday after shutdown time and if the switch is turned on. If this is not the case, the call to the turn_off_cb() will not take place.

The tests
In the first test case, I verify if the run_daily and run_every methods are called with the expected parameters and assert that the cancel_timer method is not called. In the second test I assign a value to the handles to make sure the cancel_timer method gets called.

These tests rely on the MagicMock() properties which I assigned to the workstation_fixture (see Fixtures).

Final thoughts

For some apps this might be overkill, but for other apps (like making sure your coffee machine is ready in the morning) it might not be. The most important thing is to know that it is possible to unit test your AppDaemon apps and I hope this article helped you to get an understanding on how to do it. In the final part, I’ll show you how to combine AppDaemon with an API client

--

--