HA Shell
HA Shell is a lightweight micro-framework designed to simplify the development of custom automation scripts for Home Assistant. Originally derived from a collection of Python scripts, it has been refactored into a modular and extensible architecture.
The framework promotes loosely coupled application logic and comes bundled with key utilities — including a minimalistic Home Assistant API wrapper, configuration management, structured logging, and automation helpers.
HA Shell provides a clean, consistent foundation for building maintainable and reusable automation apps, making it an ideal starting point for custom Home Assistant scripting.
Apps
An App (class) provides a base structure for your own logic. Apps are standardized on how to
parse arguments, handle runtime behavior, and receive configuration.
The base App (ha-shell.app) consists of a few simple methods that can easily be adapted to your needs.
It implements the required methods for running applications, as the base command expects.
| Method | Purpose | Required to Override |
|---|---|---|
__init__ | Sets up API, logger, and config | No |
arg_parser | Defines CLI arguments | No (but recommended) |
run | Executes app logic | Yes |
This structure provides a clean, testable, and pluggable interface for writing integration apps in a consistent manner.
Constructor
The constructor is optionally extensible and is responsible for initializing the base properties of the App.
| Name | Type | Description |
|---|---|---|
api | HassApi | Home Assistant API interface. Automatically provided. Stored as self.api. Used to perform actions or retrieve data from the HA platform. |
log | Logger | HA Shell’s logger instance. Used for logging info, errors, and debug messages. Stored as self.log. |
configuration | dict, optional | Configuration dictionary specific to the App. Stored as self.config. See Application configuration. |
Arguments
Implementing the arg_parser method allows to define your own command-line arguments. This is especially useful for
switching between run-modes or passing automation variables.
This function receives the app’s configuration, which can be useful for defining default values, e.g. coordinates or default mode.
It must always return argparse.ArgumentParser (default empty).
self is set to None during execution.
This means you cannot reference any internal members when defining arguments.Application runtime
The run-command defines the core logic of the app. Called after argument parsing and configuration loading. This
method must be overridden by subclasses to implement actual functionality.
| Parameter | Value | Purpose |
|---|---|---|
args | argparse.Namespace | Set of known arguments, as defined in arg_parser |
unknown_args | list | A list of unrecognized arguments. Useful for apps that wish to support flexible or experimental parameters. |
The run-method logs an error when it is left unimplemented.
Creating your first app
To create a new app, subclass App and implement:
- Your own
arg_parser()(optional but recommended) - Your own
run()method (required)
Examples
Example application without parameters.
# /ha-shell/apps/sumemrtime/app.py
class Summertime(App):
def run(self, args: argparse.Namespace, unknownArgs: list=None):
summertime = calc_summertime()
entity = self.api.getEntity("input_boolean.time_summertime")
if entity.state() != new_state:
entity.set_state(new_state)
self.api.setEntity(entity)
Example application with arguments and configuration.
# /ha-shell/apps/weather_forecast/app.py
class WeatherForecast(App):
def arg_parser(self, configuration=None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument('mode', help='Mode', options=['forecast', 'rain', 'history'], default="forecast")
parser.add_argument('--lat', help='Latitude', default=configuration.get("latitude"))
parser.add_argument('--lon', help='Longitude', default=configuration.get("longitude"))
return parser
def run(self, args: argparse.Namespace, unknownArgs: list=None):
match args.get('mode', 'forecast'):
case 'forecast':
params = dict(
latitude=args.lat,
longitude=args.lon,
daily=",".join(daily_attr),
hourly=",".join(hourly_attr),
timezone="Europe/Berlin",
past_days=0,
forecast_days=5,
)
response = requests.get(self.config.get('meteo_api_url'), params=params, headers={"content-type": "application/json"})
result = self.parse_forecast(response.json())
# etc
def parse_forecast(self, forecast):
pass
Features
API
| Method | Params | Return value | Comment |
|---|---|---|---|
getEntity | entity_id | Entity | raises exception if Entity doesn’t exist |
setEntity | Entity | Entity | interrogates Entity for changes and commits to HA |
callScene | scene_name | dict | sends scene call and returns raw API response |
callAutomation | automation_name | - | TODO |
callScript | script_name | - | TODO |
notifyApp | recipient, Notification | - | TODO |
Entities
The Entity class provides an extensible base for Entity management. It provides a way to unambiguously communicate
with the API and allows reading and writing state and attribute values.
The Entity class contains the following methods:
| Method | Parameters | Return value | Purpose |
|---|---|---|---|
state | - | string | int | bool | Gets current state value |
attributes | - | dict | Gets all attributes |
attribute | attribute_key, default_value | any | Get attribute value |
set_state | state | string | int | bool | Sets state |
set_attributes | attributes dict,[append bool = True] | void | Add or replace attributes |
File Manager
The FileManager module is responsible for reading configurations and writing data (work in progress) files.
It is currently not readily available to Apps, because they shouldn’t need it. This will be revised when writing files
is further developed.
Rule Engine
The RuleEngine is a powerful tool within HA Shell. It allows a familiar configuration, with an additional set of conditions, logic, and modifiers.
I wrote this in lieu of my convoluted configuration to control the movement of my vertical blinds. That consisted of an
array of lists, with each their own execution logic.
Below is an excerpt of my actual configuration showcasing most of the available rules.
## /ha-shell/config/blinds_control.yaml
# except of my actual configuration
-
- name: Dawn trigger
platform: state
entity_id: sun.sun
attribute: next_rising
modifier:
type: datetime
params:
timedelta:
hours: 1
minutes: 30
conditions:
- platform: state
name: Sun rising
entity_id: sun.sun
attribute: rising
operator: eq
value: true
operator:
operator: gte
boundary: .2
values:
.1:
pui: { _: R10 }
side: { _: R10 }
-1:
side: { _: L10 }
- name: Time trigger
platform: time
operator: eq
values:
- hour: 10
# Value condition; added for this example only
conditions:
- name: Where\'s Waldo
platform: state
entity_id: sensor.tile_keys
operator: eq
value: not_home
pui: { _: C90, heat: R45 }
side: { _: C90, heat: L45, preventOpening: true }
- hour: 0
minute: 30
pui: { _: closed }
side: { _: closed }
“Dawn trigger” might seem a bit confusing, for if sun is rising wouldn’t
next_risingbe set to tomorrow?Sun rising (bool): is derived from the elevation angle (deg) from your coordinates. -180 > -170 means it’s rising. Vice versa at solar noon ~80 deg, the value will be false.
Next rising (datetime): the day and time dawn will set in.This configuration applies a timedelta (adds time) to
next_risingand determines the time offset invalues. Usingoperator.boundarywe only allow an absolute difference of .2 hours from the value.
All rule-related keys are stripped before passing the matching action back. This ensures that, in this case,
blinds_control only receives the relevant keys (pui, side) for controlling the blinds.
The RuleEngine is still fairly limited, as I only needed support for entity- and time-based rules.
I might take a closer look at the HA source code to explore further capabilities.
Development & configuration
HA Shell can easily be run as a python module —inside Docker for example:
docker compose exec homeassistant python -m hash calendar -f "my event" --calendar default -n 2021-01-01 -m 2025-12-31
The HA configuration looks like this:
# configuration.yaml
shell_command:
ha-shell: "python -m ha-shell {{action}} {{args}}"
Which in turn can be triggered using an automation:
trigger: ~
actions:
- platform: shell_command
action: ha-shell
data:
action: calendar
args: -f {{states('input_text.calendar_search')}} --calendar {{'input_text.calendar_search_cal'}} -n {{states('input_date.calendar_search_start')}} -m {{states('input_date.calendar_search_end')}}
Application configuration
HA Shell and Applications are configured using YAML files within /ha-shell/config/. Please adhere to the following
conventions:
| File | Purpose | Required |
|---|---|---|
| config.yaml | HA Shell main configuration | Yes |
| your_app.yaml | Application specific configuration | No |
Configuration variables
WORK IN PROGRESS
HA Shell expects config.yaml to exists. This must contain at least the following variables:
| Variable | Purpose | Values |
|---|---|---|
| loglevel | Sets logging verbosity | error, info, debug |
| apps | list of installed apps | [my_app, my_other_app] |
The API will look for host and token configurations in Home Assistant’s secrets.yaml.