HA Shell

18 Jun, 2025 · 7 min read
HA Shell is still a work in progress. The code is yet to be published and the documentation will be adapted accordingly.

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.

MethodPurposeRequired to Override
__init__Sets up API, logger, and configNo
arg_parserDefines CLI argumentsNo (but recommended)
runExecutes app logicYes

This structure provides a clean, testable, and pluggable interface for writing integration apps in a consistent manner.

Strictly speaking, the run-command doesn’t have to be implemented. It will log an error message, but otherwise continue execution.

Constructor

The constructor is optionally extensible and is responsible for initializing the base properties of the App.

NameTypeDescription
apiHassApiHome Assistant API interface. Automatically provided. Stored as self.api. Used to perform actions or retrieve data from the HA platform.
logLoggerHA Shell’s logger instance. Used for logging info, errors, and debug messages. Stored as self.log.
configurationdict, optionalConfiguration 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).

Due to the nature of Python, static methods don’t exist, the 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.

ParameterValuePurpose
argsargparse.NamespaceSet of known arguments, as defined in arg_parser
unknown_argslistA 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

MethodParamsReturn valueComment
getEntityentity_idEntityraises exception if Entity doesn’t exist
setEntityEntityEntityinterrogates Entity for changes
and commits to HA
callScenescene_namedictsends scene call and returns raw API response
callAutomationautomation_name-TODO
callScriptscript_name-TODO
notifyApprecipient,
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:

MethodParametersReturn valuePurpose
state-string | int | boolGets current state value
attributes-dictGets all attributes
attributeattribute_key, default_valueanyGet attribute value
set_statestatestring | int | boolSets state
set_attributesattributes dict,
[append bool = True]
voidAdd 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_rising be 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_rising and determines the time offset in values. Using operator.boundary we 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.

Caching rules will be implemented in the near future to better allow time offsets; so you don’t accidentally reference tomorrow/yesterday’s sun-time-event**

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:

FilePurposeRequired
config.yamlHA Shell main configurationYes
your_app.yamlApplication specific configurationNo
Per the Python standard file names and modules are snake_cased

Configuration variables

WORK IN PROGRESS

HA Shell expects config.yaml to exists. This must contain at least the following variables:

VariablePurposeValues
loglevelSets logging verbosityerror, info, debug
appslist of installed apps[my_app, my_other_app]

The API will look for host and token configurations in Home Assistant’s secrets.yaml.