Skip to content

Send anything to Home Assistant, through MQTT, powered by Go.

License

Notifications You must be signed in to change notification settings

joshuar/go-hass-anything

Repository files navigation

Go Hass Anything

Send anything to Home Assistant, through MQTT, powered by Go.

Contributors Shield Last Update Shield Forks Shield Stars Shield Open Issues Shield License Shield


📓 Table of Contents

🌟 About the Project

Go Hass Anything is a framework for writing self-contained apps in Go that can send data and listen for controls to/from Home Assistant, over MQTT. This can be useful for adding sensors or controls to Home Assistant that are not available through an existing Home Assistant integration.

The code is flexible to be imported as a package into your own Go code to provide this functionality, or it can be run as its own “agent” process that will manage any apps you write.

The agent is extremely light on resources, using only a few tens of megabytes of memory at most. As the agent and apps are written in Go, it can run on a wide variety of platforms from embedded through to server hardware.

👾 Tech Stack

Agent
DevOps

🎯 Features

  • Write self-contained “apps” in Go that are run by the agent.
  • Apps can specify either a polling interval that the agent will run the app on to publish updates to MQTT, or, pass a channel back to the agent and send events that the agent will publish on MQTT.
  • Apps can optionally specify user-facing preferences that the agent will present via a terminal UI for configuration.
  • Apps can use the following Home Assistant entities:
  • Simple TOML based configuration.
  • Compile all apps into a single binary.
  • Use via a container or stand-alone binary.
  • Light on resources (CPU/memory).
  • Runs anywhere that Go runs, from embedded to server hardware.

🗒️ Versioning

This project follows semantic versioning. Given a version number MAJOR.MINOR.PATCH, the gist of it is:

  • A MAJOR number change means breaking changes from the previous release.
  • A MINOR number change means significant changes and new features have been added, but not breaking changes.
  • A PATCH number change indicate minor changes and bug fixes.

🧰 Getting Started

‼️ Prerequisites

Go Hass Anything uses Mage for development. Make sure you follow the instructions on the Mage website to install Mage. If you are using the devcontainer (see below), this is already installed.

🚧 Development Environment

It is recommended to use Visual Studio Code. This project makes use of a Devcontainer to provide some convenience during development.

Open in Dev Containers

If using Visual Studio Code, you should be prompted when opening your cloned copy of the code to set up the dev container. The container contains an installation of Home Assistant and Mosquitto (MQTT broker) that can be used for testing. They should be started automatically.

An example configuration for Mosquitto has been provided in deployments/mosquitto/config/mosquitto.conf.example.

The Mosquitto command-line utilities (mosquitto_{pub,sub}) are installed in the devcontainer.

⚙️ Building

Note

If you have not yet created an app, Go Hass Anything will build with an included example app. See the app creation instructions below for details on creating and including your own apps.

Use the following mage invocation in the project root directory:

mage -d build/magefiles -w . build:full

This will:

  • Run go generate ./....
  • Run go mod tidy.
  • Run go fmt ./....
  • Build a binary and place it in dist/go-hass-anything.

To just build a binary, replace build:full with build:fast in the mage invocation above.

To see all possible build commands, run:

mage -d build/magefiles -w . -l

Cross compilation should work as per normal for Go. To build for a particular architecture, set the TARGETARCH environment variable to the equivalent GOARCH value when running the mage build command above:

# Set TARGETARCH as appropriate, i.e., amd64 or arm64 or arm
TARGETARCH=arm64 mage -d build/magefiles -w . build:full

🚩 Deployment

While Go Hass Anything can be run as a single binary, using a container is recommended. podman is the container engine of choice for deployment.

A Dockerfile is available that you can use to build an image containing your own custom apps.

To add your own apps to the container, copy them into a directory in the base of the repo (for example, apps/) and then specify the build arg APPDIR pointing to this location:

podman build --file ./Dockerfile --tag go-hass-anything --build-arg APPDIR=apps

By default, the container will run as a user with uid/gid 1000/1000. You can pick a different uid/gid when building by adding --build-arg UID=999 and --build-arg GID=999 (adjusting the values as appropriate).

Cross compilation is supported. For example, to build for multiple architectures:

podman build --file ./Dockerfile \
  --tag go-hass-anything \
  --build-arg APPDIR=apps \
  --platform=linux/arm64,linux/armv7,linux/amd64

Pre-built containers that can run some demo apps can be found on the packages page on GitHub. The demo app source code can be found in examples/.

🏃 Running

🔧 Configuration

To run the agent, you first need to configure the MQTT connection. Use the command:

# For containers:
podman run --interactive --tty --rm \
    --volume ~/go-hass-anything:/home/go-hass-anything:U \
    ghcr.io/joshuar/go-hass-anything configure
# For binaries:
go-hass-anything configure

This will open a user interface in the terminal to enter MQTT connection details for the agent, and then any preferences for apps. You can navigate the fields via the keyboard.

👀 Usage

Once the agent is configured, you can run it. Use the command:

# For containers:
podman run --name my-go-hass-anything \
    --volume ~/go-hass-anything:/home/go-hass-anything:U \
    ghcr.io/joshuar/go-hass-anything
# For binaries:
go-hass-anything run

♻️ Reset

If needed/desired, you can remove the app entities from Home Assistant by running the command:

# For containers:
podman run --interactive --tty --rm \
    --volume ~/go-hass-anything:/home/go-hass-anything:U \
    ghcr.io/joshuar/go-hass-anything clear
# For binaries:
go-hass-anything clear

After this, there should be no devices (from Go Hass Anything) and associated entities in Home Assistant. If you want to re-add them, execute the run command again.

💻 Development

💽 Building Apps

Note

Check out the examples which a few of the different types of entities you can create in Home Assistant.

Code Location

Important

The app directory is not committed to version control. This allows your apps to remain private. But it also means that if you desire version control of your apps, you should set up your own repo for them.

You can put your code in apps/. You can create multiple directories for each app you develop.

Note

The filename is important. The generator to automatically add your app to the agent needs a .go file named the same as the app directory to detect your app. Make sure you at least have this file if you split your app code into multiple files.

App Requirements

To develop an app to be run by the agent, create a concrete type that satisfies the agent.App interface:

// App represents an app that the agent can run. All apps have the following
// methods, which define how the app should be configured, current states of its
// entities and any subscriptions it wants to watch.
type App interface {
  // Name() is an identifier for the app, used for logging in the agent.
  Name() string
  // Configuration() returns the messages needed to tell Home Assistant how to
  // configure the app and its entities.
  Configuration() []*mqtt.Msg
  // States() are the messages that reflect the app's current state of the
  // entities of the app.
  States() []*mqtt.Msg
  // Subscriptions() are the topics on which the app wants to subscribe and
  // execute a callback in response to a message on that topic.
  Subscriptions() []*mqtt.Subscription
  // Update() is a function that is run at least once by the agent and will
  // usually contain the logic to update the states of all the apps entities.
  // It may be run multiple times, if the app is also considered a polling
  // app. See the definition for PollingApp for details.
  Update(ctx context.Context) error
}
  • You don't need to worry about setting up a connection to MQTT, the agent will do that for you.
  • Name(): This should return the app name as a string. This is used for defining the app configuration file (if used) and in various places for display by the agent.
  • Configuration() []*mqtt.Msg: This function should return an array of mqtt.Msg, each message representing the configuration topics and details for the sensors provided by the app.
  • States() []*mqtt.Msg: This function should return an array of mqtt.Msg, each message representing a single state topic for a sensor provided by the app.
  • Subscriptions []*mqtt.Subscription: This function should return an array of mqtt.Subscription, each message representing a single subscription topic for which the app wants to listen on. Each of these subscriptions should have a callback function that is run when a message is received on the topic.
  • Update(ctx context.Context) error: This function will be called by the agent at least once. It can be used to update any app state before the agent publishes app state messages to MQTT. It should respect context cancellation and act appropriately on this signal.

Create an exported function called New that is used to instantiate your app with the signature:

func New(ctx context.Context) (*yourAppStruct, error)

This function should return your concrete type that satisfies the interface methods above, or an error if the app cannot be initialised. You can put whatever code you need in this function to set up your application (i.e., reading from configs, setting up other connections, etc.). This will be called first by the agent to initialise your app.

Poll based Apps

If the app should be run on some kind of interval, updating its state each time, it should have the following method:

// PollingApp represents an app that should be polled for updates on some
// interval. When an app satisfies this interface, the agent will configure a
// goroutine to run the apps Update() function and publish its States().
type PollingApp interface {
  // PollConfig defines the interval on which the app should be polled and its
  // states updated. A jitter should be defined, that is much less than the
  // interval, to add a small variation to the interval to avoid any
  // "thundering herd" problems.
  PollConfig() (interval, jitter time.Duration)
}

Event based Apps

If the app has its own event loop, and requires states to be published when certain events occur, it should have the following method:

// EventsApp represents an app that will update its States() in response to some
// event(s) it is monitoring. When an app satisfies this interface, the agent
// will configure a goroutine to watch a channel of messages the app sends when
// an event occurs, which will be published to MQTT.
type EventsApp interface {
  // MsgCh is a channel of messages that the app generates when some internal
  // event occurs and a new message should be published to MQTT.
  MsgCh() chan *mqtt.Msg
}

In the app code (usually within New()), the app should create a chan *mqtt.Msg, returned by the method above. Any time a state update needs to be published, it can be sent through this channel and the agent will publish the message on MQTT.

(Optional) App Configuration

If your app has user-facing configuration, the agent supports presenting these to the user when its configuration command is run. It will then create and utilise a per-app configuration stored in the users home directory (~/.config/go-hass-anything/APPNAME-preferences.toml on Linux).

For your app to support this, make sure it satisfies the AppWithPreferences interface:

// AppWithPreferences represents an app that has preferences that can be
// configured by the user.
type AppWithPreferences interface {
  App
  // DefaultPrefernces returns the AppPreferences map of default preferences for the app.
  // This is passed to the UI code to facilitate generating a form to enter
  // the preferences when the agent runs its configure command.
  DefaultPreferences() (preferences.AppPreferences)
}

Each app preference can be represented as a preference.Preference:

// Preference represents a single preference in a preferences file.
type Preference struct {
  // Value is the actual preference value.
  Value any `toml:"value"`
  // Description is a string that describes the preference, and may be used
  // for display purposes.
  Description string `toml:"description,omitempty"`
  // Secret is a flag that indicates whether this preference represents a
  // secret. The value has no effect on the preference encoding in the TOML,
  // only on how to display the preference to the user (masked or plaintext).
  Secret bool `toml:"-"`
}

The agent takes care of loading and saving the configuration. When the agent is configured or run for the first time, the agent will show/use default preferences for each app.

Adding to the agent

If you have followed the requirements above for both location and code functions, you can run go generate ./... in the repo root to add your app(s) to the agent. A new internal/agent/init.go file should be generated, which will contain the necessary code to run your apps to the agent.

Important

The file internal/agent/init.go is not committed to version control. Like your app code, this allows your apps to remain private.

After building the agent, it should run all of your apps.

Logging

All packages use log/slog for logging, so if including the Go Hass Anything packages in your own code, you can hook into and/or extend upon that. Note that some of the packages define custom levels for trace (level -8) and fatal (level 12), which if the logger is set to output, will show some additional details from the internals.

👋 Contributing

Go Hass Anything Contributors

Contributions are always welcome!

See CONTRIBUTING.md for ways to get started.

🏁 Committing Code

This repository is using conventional commit messages. This provides the ability to automatically include relevant notes in the changelog. The TL;DR is when writing commit messages, add a prefix:

  • feat: for a new feature, like a new sensor.
  • fix: when fixing an issue.
  • refactor: when making non-visible but useful code changes.
  • …and so on. See the link above or see the existing commit messages for examples.

📜 Code of Conduct

Please read the Code of Conduct

⚠️ License

Distributed under the MIT license.

🤝 Contact

Joshua Rich - @joshuar

Project Link: https://github.com/joshuar/go-hass-anything

💎 Acknowledgements