Hooked on Hass

18 Jun, 2025·
Valentijn Verhallen
Valentijn Verhallen
· 12 min read

I’ve been using Home Assistant for a while now to handle various simple automations around the house. One of my recent additions includes a set of ESPresense sensors paired with Tile tags to detect when I come and go. This setup automatically adjusts the vertical blinds and controls the lights.

The blinds rotate based on the sun’s position and the time of day. Since my windows face south, I tend to keep them partially closed when it’s sunny or warm to reduce heat buildup. In a previous home, I experimented with automating a sunshade based on weather data, but found it unreliable—external data just wasn’t consistent enough for preemptive control. A personal weather station would’ve helped, but building one that’s reliable enough is a challenge in itself.

Each set of blinds has its own preferred rotation angles and open positions, so I was already using a small Python script to compile and trigger the correct scene on my Hunter Douglas (Luxaflex) hub.
As I kept adding more conditions and triggers, the number of automations grew rapidly. Managing a heat protocol became especially tricky, since I had no easy way to set it in advance — it was all reactive and manual — and adjust the blinds accordingly.

At this point, the UI and templating in Home Assistant started to feel limiting. As a programmer, I know what I want and how I’d like to express logic — but faffing around with arbitrary Jinja templates just doesn’t cut it anymore.

Time for a change.

A quick word on Home Assistant

I really appreciate Home Assistant — it’s impressive how much functionality is available for free. That said, I’ve noticed the documentation can sometimes be a bit limited, especially compared to how it was a few years ago. Some of the unofficial packages also tend to be offered “as is,” which can make support feel a bit hands-off. It might just be a matter of not knowing the right terminology, but when working on projects that go beyond the basics, it can be challenging to find clear guidance. In those cases, I often have better luck using a search engine than relying solely on the official docs. That’s one of the reasons I want to take a deeper dive into my design process — it helps clarify things and might even contribute back to the community.

Templates

Although I’m not a big fan, it’d be unfair not to give some credit to Templating. These give the user an advanced control over automations (blueprints) and UI representation. I use some templates for minor automations and UI elements, and won’t discuss this subject any further. Please read the docs for more details. I find it easiest to use a search engine when I need something specific, as a lot of things have already been created.

Python scripts

The first obvious option HA provides is the use of Python Scripts. They are easily configured in configuration.yaml and are stored under /config/python_scripts. Python scripts are sandboxed, giving you access to a few global variables to read and edit HA entities, among others.

As described in my foreword, I was already a few scripts. Although you can pass data to scripts quite easily, I prefer managing states with input helpers. I use a small automation that listens to changes, before triggering the script. The script then gets the entity’s state using the hass.states API.

Using python scripts is useful for basic automation, that can’t (easily) be settled through the UI/templates. Maintaining my blinds meant I had to compile the scene name out of a small series of variables. Due to how easy it is to edit Python in contrast to Jinja, I automatically opt for scripts. Full disclosure: I wasn’t all too familiar with Jinja at the time.

Shell command

Although I’m getting ahead of myself, I’d like to explain a bit about Shell commands. These give you more freedom to manage your own code, by allowing any accessible command. That’s an important keyword: accessible. Shell commands are run from the /config directory. Depending on your host, this means you have a limited set of available commands. This is especially true if you’re using Docker, like me, as the default docker image lives in a Debian shell and only has Python installed.

Of course, this introduces new challenges: you lose the sandbox with its built-in access to HA helpers, and now you’re writing real Python. But you can now split up code, reuse logic, and use classes. If you’re willing, you can even compile your own Docker image —installing whichever programming language. This is even easier if you’re running bare metal. You could also opt for proxying your commands outside the host.

At first, I myself pondered on using a PHP API. Because I had already written the necessary logic in python scripts by that time, I opted for creating a proper Python Module.

AppDeamon

Then there’s also AppDaemon, acting as a front-controller for your commands. I don’t have any in-depth info on AppDeamon, but please keep reading, because this gets interesting.

At the time of developing, I thought that this would be overkill. Besides, “I only need a handful of scripts, right?”,
–slight foreshadowing–

Let’s get technical

I needed to quickly overhaul how I control my blinds. I also needed a reliable weather forecast source, because the default implementation in HA is so locked down that you can’t extract a usable heat protocol from it.

Blinds control

There are two key principles to consider: overrides and reverse rotation (reopening).

Overrides

I wanted to be able to manually change the rotation without it being immediately overwritten by the next automation. These overrides have a TTL (time to live) of 3 hours. Before any automated rotation runs, it checks whether the override (via a number helper) is greater than the current hour.

Reopening

Because I use both the sun’s position and time-based criteria, the blinds may already be rotated beyond the intended new position. For this, I implemented a prevent_opening: bool. This flag is set by certain triggers to prevent the blinds from being reopened unnecessarily.

Helpers

I was already using helper inputs to determine blinds rotation and positions. This key aspect hasn’t changed throughout the process. I only added helpers to maintain overrides per blind. Sadly, helpers don’t have native support for key-value maps, at least not to be used by the UI.

I use custom inputs to manage/select the legal rotations for each blind. For example:

# /config/custom/input/select/blinds_side_rotation.yaml

## Each position is the rotation in degrees
## Left (/), Center (|), Right (\)
name: Blinds Side rotation
options:
  - L20
  - L45
  - C90
  - R45
  - R20
  - R10
  - closed
initial: C90
icon: mdi:rotate-360

This input used to also be present on the dashboard, to manually set the position. You also have a remote, of course, but because of the automation the setting will be overridden.
I now only represent its value in the UI.

Bringing it all together

At first, I created a new Python script: /config/python_scripts/blinds_control.py. I wrote a fairly extensive configuration in there to handle the right triggers. Using the sandboxed API, I could retrieve all the necessary data to determine positioning and store the new states for my helper inputs. Those in turn would trigger the existing scripts running the scene trigger.

This script got triggered every 5 minutes to check prerequisites.

Meteo weather forecast

Another Python script handled the weather forecasts. Many APIs are paid and require info I’d rather not share. I found Meteo, which pulls Dutch forecasts from KNMI. It doesn’t require any credentials but does have rate limits. Since I don’t need to fetch the weather that often, this is fine for my use case.
To avoid hitting the rate limit during testing, I used a JSON file as test input.

Meteo provides data in a rather odd format, but with some clever slicing logic, you don’t need to iterate over all the addresses.

Ultimately, I extract the heat protocol for the next 3 hours, based on cloud cover, sunshine hours, and temperature. I include this as part of the attributes and publish it to a new entity. Depending on your installation/integrations, Home Assistant might report an unknown entity, but shouldn’t mind this. It’s important to run this script on startup though, as it does disappear after a reboot.

Apart from a bunch of configuration and request params, this component isn’t that interesting. It results in a big JSON blob of attributes, which aren’t usable by HA. I publish some state/attribute info commonly found in Weather platforms, so it gets rudimentary UI support.

It would be awesome to eventually turn this into a custom component.

Meteo already has its own Weather integration. My main problem is that the Weather platform shields off most its values.

Summertime

Due to daylight saving time, the sun’s position and dawn/dusk times shift. I wrote a small script that checks the if the current date is in range of DST. This script runs daily around midnight and toggles a boolean helper.

This helper can be used to shift some of the trigger values on initialisation.

Evaluation

It worked, but it wasn’t very convenient. The config is embedded in the code, the sandbox doesn’t support classes, and you can’t use imports.

Some triggers in Blinds control didn’t behave as expected, and the config was hard to tweak. Python’s strict indentation makes it easy to introduce syntax errors.

Also, something went wrong with the helper inputs: during an update, you have to send both the state and attributes. If you don’t, the helper breaks in the UI. In my opinion, this is a major flaw in the HA API. Fortunately, this is easy to fix with a small function in the scripts—but since you can’t use imports, you end up with a lot of duplicated code.

Revision 1: Using Shell Commands

To get back in control, it was time to start writing shell commands. Given the challenges discussed earlier, this required a bit more setup.

API Module

I started by writing a HA API class, so I could talk to HA. I only needed a few methods, so it was relatively straightforward. I’d already learned that the requests library offers a very user-friendly HTTP client. Unlike, say, vanilla PHP, you just call a method and pass headers with Python’s named arguments.

To make life easier, I immediately wrote an Entity class to maintain state and attributes. Partly to follow OOP principles, but also to simplify future code extensions.

Home Assistant manages nearly everything as an “entity”, which gives you lots of flexibility in implementation. The key is to remember that when updating an entity (via a POST request), you need to send both the state and the attributes. This means you need to fetch them first.
The API/Entity classes help with this logic, at it state and attributes automatically get (un)serialized and remembered.

I also thought it’d be smart to maintain an entity registry —an in-memory mapping of entity_id => entity. This avoids repeated API calls to fetch attributes.

In the end, the API only needed to retrieve/update entities and trigger scenes. That was simple to implement with just a few (chained) methods.

Configuration

I also created an extra module called ConfigReader to read custom configs and HA secrets. This let me move all config out of the code and into YAML/JSON files. Python has native support for this too. I started to really enjoy using the language!

Logging

A new problem arose. You can’t access the native HA log anymore. I didn’t want to hack my way into it, as this might break the native log stream, so I created a new Logger module. This writes daily logs to its own directory. Of course, I added support for log levels through a configuration entry, as this would soon be practical.

Bringing it all together again

I went ahead and replaced my scripts with shell commands. I used the API to communicate with HA. I added a new Blind subclass to Entity, where I moved all the rotation and override logic. This let the command script act purely as a controller.

I configured the commands in HA:

# configuration.yaml
shell_command:
  blinds_control: python python_scripts/blinds_control.py {{action}} {{data}}
  weather_forecast: python python_scripts/weather_forecast.py
  summertime: python python_scripts/summertime.py
  # more...

Second Evaluation

Everything seemed to work a lot better, but the file structure was a bit of a mess. It also became increasingly hard to implement and debug, as I had to restart HA every time I did a code update.

I thought, I’d better implement some kind of front-controller — a sort of umbrella — to capture all commands, so I don’t have to reload my configurations anymore.

Revision 2; the birth of HA Shell

I decided to build a front controller. This allowed me to invoke any underlying command and, when needed, pass custom arguments. I used argparse to handle the initial CLI arguments and the subcommand-action. Since some subcommands also required their own arguments, I made it possible to retrieve these during initialization.

I named the resulting command Hash, short for Home Assistant Shell. This later became HA Shell (ha-shell) to prevent module collisions.

After a few minor revisions, I moved my subcommands away from orphaned methods, to enforce an App architecture, ensuring every app follows a consistent signature. Apps now live in their respective apps directory. As a bonus, each app automatically receives its own configuration — if one exists.

I also significantly reworked the system behind blinds_control. Instead of a complex configuration structure for handling different types of triggers, I implemented a RuleEngine that interprets platform triggers in a manner similar to how HA templates function. This allows for clear, consistent configuration that can be reused across various purposes.

My implementation supports not only traditional conditions but also value-based conditions, and it can perform calculations and monitor threshold values.

Please read the in-depth post on HA Shell for more information.

Conclusion

I’ve essentially recreated AppDaemon — with a bit of sparkle.

My head is full of ideas, so I’m planning to build a custom Home Assistant image soon. That way, I can integrate services like Redis directly. I’ve already tested this by installing the Redis pip package inside the running image. To support this, I’ve placed Home Assistant in a macvlan network. But that’s a story for another time.

That leaves just some final configuration. My test period has only lasted a few days so far, so there are likely still some adjustments to make. I’m also curious to see how daylight saving time will affect things this time around. Since I’m now handling that automatically, I can adapt the configuration at the start of "blinds_control" if necessary. To be continued.

I plan to publish the code for HA Shell soon — or, in classic developer terms: soon™.

–> Go to Project HA Shell