Send anything to Home Assistant, through MQTT, powered by Go.
- 📓 Table of Contents
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.
- 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:
- Sensor (Example App)
- Binary Sensor
- Switch (Example App)
- Button (Example App)
- Number (Example App)
- Text (Example App)
- Image (Example App)
- Camera
- With more to come!
- 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.
Note
If you are looking to add sensors for a Linux desktop/server to Home Assistant, check out Go Hass Agent which uses Go Hass Anything under the hood!
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.
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.
It is recommended to use Visual Studio Code. This project makes use of a Devcontainer to provide some convenience during development.
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.
- Home Assistant will be listening on http://localhost:8123.
- Mosquitto will be listening on http://localhost:1833.
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.
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
Go Hass Anything can also be built for arm (v6/v7) and arm64 with
cross-compilation. To build for a different architecture, set the
TARGETPLATFORM
environment variable:
export TARGETPLATFORM=linux/arm64 # or linux/arm/v6 or linux/arm/v7
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
As with building a binary, cross-compliation is supported:
# use either linux/arm64, linux/arm/v7 or linux/arm/v6
podman build --file ./Dockerfile --platform linux/arm/v7 --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).
Pre-built containers that can run some demo apps showing some of the available entities can be found on the packages page on GitHub. The demo app source code can be found in examples/.
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.
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
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.
Check out the examples which a few of the different types of entities you can create in Home Assistant.
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 expects a file named
main.go
in app directories for those directories to be considered as an app. Make sure you at least have this file if you split your app code into multiple files.
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 ofmqtt.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 ofmqtt.Msg
, each message representing a single state topic for a sensor provided by the app.Subscriptions []*mqtt.Subscription
: This function should return an array ofmqtt.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.
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)
}
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.
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.
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.
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.
Contributions are always welcome!
See CONTRIBUTING.md for ways to get started.
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.
Please read the Code of Conduct
Distributed under the MIT license.
Joshua Rich - @joshuar
Project Link: https://github.com/joshuar/go-hass-anything