diff --git a/README.md b/README.md
index a809ae42..bc52cf04 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
- [Configuring Gotify alerts](#configuring-gotify-alerts)
+ - [Configuring HomeAssistant alerts](#configuring-homeassistant-alerts)
- [Configuring Incident.io alerts](#configuring-incidentio-alerts)
- [Configuring JetBrains Space alerts](#configuring-jetbrains-space-alerts)
- [Configuring Matrix alerts](#configuring-matrix-alerts)
@@ -604,6 +605,7 @@ endpoints:
| `alerting.telegram` | Configuration for alerts of type `telegram`.
See [Configuring Telegram alerts](#configuring-telegram-alerts). | `{}` |
| `alerting.twilio` | Settings for alerts of type `twilio`.
See [Configuring Twilio alerts](#configuring-twilio-alerts). | `{}` |
| `alerting.zulip` | Configuration for alerts of type `zulip`.
See [Configuring Zulip alerts](#configuring-zulip-alerts). | `{}` |
+| `alerting.homeassistant` | Configuration for alerts of type `homeassistant`.
See [Configuring HomeAssistant alerts](#configuring-homeassistant-alerts). | `{}` |
#### Configuring AWS SES alerts
@@ -920,6 +922,75 @@ Here's an example of what the notifications look like:

+#### Configuring HomeAssistant alerts
+To configure HomeAssistant alerts, you'll need to add the following to your configuration file:
+
+```yaml
+alerting:
+ homeassistant:
+ url: "http://homeassistant:8123" # URL of your HomeAssistant instance
+ token: "YOUR_LONG_LIVED_ACCESS_TOKEN" # Long-lived access token from HomeAssistant
+
+endpoints:
+ - name: my-service
+ url: "https://my-service.com"
+ interval: 5m
+ conditions:
+ - "[STATUS] == 200"
+ alerts:
+ - type: homeassistant
+ enabled: true
+ send-on-resolved: true
+ description: "My service health check"
+ failure-threshold: 3
+ success-threshold: 2
+```
+
+The alerts will be sent as events to HomeAssistant with the event type `gatus_alert`. The event data includes:
+- `status`: "triggered" or "resolved"
+- `endpoint`: The name of the monitored endpoint
+- `description`: The alert description if provided
+- `conditions`: List of conditions and their results
+- `failure_count`: Number of consecutive failures (when triggered)
+- `success_count`: Number of consecutive successes (when resolved)
+
+You can use these events in HomeAssistant automations to:
+- Send notifications
+- Control devices
+- Trigger scenes
+- Log to history
+- And more
+
+Example HomeAssistant automation:
+```yaml
+automation:
+ - alias: "Gatus Alert Handler"
+ trigger:
+ platform: event
+ event_type: gatus_alert
+ action:
+ - service: notify.notify
+ data_template:
+ title: "Gatus Alert: {{ trigger.event.data.endpoint }}"
+ message: >
+ Status: {{ trigger.event.data.status }}
+ {% if trigger.event.data.description %}
+ Description: {{ trigger.event.data.description }}
+ {% endif %}
+ {% for condition in trigger.event.data.conditions %}
+ {{ '✅' if condition.success else '❌' }} {{ condition.condition }}
+ {% endfor %}
+```
+
+To get your HomeAssistant long-lived access token:
+1. Open HomeAssistant
+2. Click on your profile name (bottom left)
+3. Scroll down to "Long-Lived Access Tokens"
+4. Click "Create Token"
+5. Give it a name (e.g., "Gatus")
+6. Copy the token - you'll only see it once!
+
+
#### Configuring Incident.io alerts
| Parameter | Description | Default |
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
diff --git a/alerting/alert/type.go b/alerting/alert/type.go
index 3875d4d8..1f36d6e1 100644
--- a/alerting/alert/type.go
+++ b/alerting/alert/type.go
@@ -32,6 +32,9 @@ const (
// TypeGotify is the Type for the gotify alerting provider
TypeGotify Type = "gotify"
+ // TypeHomeAssistant is the Type for the homeassistant alerting provider
+ TypeHomeAssistant Type = "homeassistant"
+
// TypeIncidentIO is the Type for the incident-io alerting provider
TypeIncidentIO Type = "incident-io"
diff --git a/alerting/config.go b/alerting/config.go
index 8378a840..58c1d647 100644
--- a/alerting/config.go
+++ b/alerting/config.go
@@ -15,6 +15,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
+ "github.com/TwiN/gatus/v5/alerting/provider/homeassistant"
"github.com/TwiN/gatus/v5/alerting/provider/incidentio"
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
@@ -62,6 +63,9 @@ type Config struct {
// Gotify is the configuration for the gotify alerting provider
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
+ // HomeAssistant is the configuration for the homeassistant alerting provider
+ HomeAssistant *homeassistant.AlertProvider `yaml:"homeassistant,omitempty"`
+
// IncidentIO is the configuration for the incident-io alerting provider
IncidentIO *incidentio.AlertProvider `yaml:"incident-io,omitempty"`
diff --git a/alerting/provider/homeassistant/homeassistant.go b/alerting/provider/homeassistant/homeassistant.go
new file mode 100644
index 00000000..1b4ad684
--- /dev/null
+++ b/alerting/provider/homeassistant/homeassistant.go
@@ -0,0 +1,196 @@
+package homeassistant
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/TwiN/gatus/v5/alerting/alert"
+ "github.com/TwiN/gatus/v5/client"
+ "github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
+)
+
+var (
+ ErrURLNotSet = errors.New("url not set")
+ ErrTokenNotSet = errors.New("token not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ URL string `yaml:"url"`
+ Token string `yaml:"token"`
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.URL) == 0 {
+ return ErrURLNotSet
+ }
+ if len(cfg.Token) == 0 {
+ return ErrTokenNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.URL) > 0 {
+ cfg.URL = override.URL
+ }
+ if len(override.Token) > 0 {
+ cfg.Token = override.Token
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using HomeAssistant
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of Override that may be prioritized over the default configuration
+ Overrides []Override `yaml:"overrides,omitempty"`
+}
+
+// Override is a case under which the default integration is overridden
+type Override struct {
+ Group string `yaml:"group"`
+ Config `yaml:",inline"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ registeredGroups := make(map[string]bool)
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
+ }
+ registeredGroups[override.Group] = true
+ }
+ }
+ return provider.DefaultConfig.Validate()
+}
+
+// Send an alert using the provider
+func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/events/gatus_alert", cfg.URL), buffer)
+ if err != nil {
+ return err
+ }
+ request.Header.Set("Content-Type", "application/json")
+ request.Header.Set("Authorization", "Bearer "+cfg.Token)
+ response, err := client.GetHTTPClient(nil).Do(request)
+ if err != nil {
+ return err
+ }
+ defer response.Body.Close()
+ if response.StatusCode > 399 {
+ body, _ := io.ReadAll(response.Body)
+ return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
+ }
+ return err
+}
+
+type Body struct {
+ EventType string `json:"event_type"`
+ EventData struct {
+ Status string `json:"status"`
+ Endpoint string `json:"endpoint"`
+ Description string `json:"description,omitempty"`
+ Conditions []struct {
+ Condition string `json:"condition"`
+ Success bool `json:"success"`
+ } `json:"conditions,omitempty"`
+ FailureCount int `json:"failure_count,omitempty"`
+ SuccessCount int `json:"success_count,omitempty"`
+ } `json:"event_data"`
+}
+
+// buildRequestBody builds the request body for the provider
+func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+ body := Body{
+ EventType: "gatus_alert",
+ EventData: struct {
+ Status string `json:"status"`
+ Endpoint string `json:"endpoint"`
+ Description string `json:"description,omitempty"`
+ Conditions []struct {
+ Condition string `json:"condition"`
+ Success bool `json:"success"`
+ } `json:"conditions,omitempty"`
+ FailureCount int `json:"failure_count,omitempty"`
+ SuccessCount int `json:"success_count,omitempty"`
+ }{
+ Status: "resolved",
+ Endpoint: ep.DisplayName(),
+ },
+ }
+
+ if !resolved {
+ body.EventData.Status = "triggered"
+ body.EventData.FailureCount = alert.FailureThreshold
+ } else {
+ body.EventData.SuccessCount = alert.SuccessThreshold
+ }
+
+ if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
+ body.EventData.Description = alertDescription
+ }
+
+ if len(result.ConditionResults) > 0 {
+ for _, conditionResult := range result.ConditionResults {
+ body.EventData.Conditions = append(body.EventData.Conditions, struct {
+ Condition string `json:"condition"`
+ Success bool `json:"success"`
+ }{
+ Condition: conditionResult.Condition,
+ Success: conditionResult.Success,
+ })
+ }
+ }
+
+ bodyAsJSON, _ := json.Marshal(body)
+ return bodyAsJSON
+}
+
+// GetDefaultAlert returns the provider's default alert configuration
+func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
+ return provider.DefaultAlert
+}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ if len(alert.ProviderOverride) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.ProviderOverrideAsBytes(), &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ err := cfg.Validate()
+ return &cfg, err
+}
+
+// ValidateOverrides validates the alert's provider override and, if present, the group override
+func (provider *AlertProvider) ValidateOverrides(group string, alert *alert.Alert) error {
+ _, err := provider.GetConfig(group, alert)
+ return err
+}
diff --git a/alerting/provider/homeassistant/homeassistant_test.go b/alerting/provider/homeassistant/homeassistant_test.go
new file mode 100644
index 00000000..3bc04ac5
--- /dev/null
+++ b/alerting/provider/homeassistant/homeassistant_test.go
@@ -0,0 +1,158 @@
+package homeassistant
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/TwiN/gatus/v5/alerting/alert"
+ "github.com/TwiN/gatus/v5/client"
+ "github.com/TwiN/gatus/v5/config/endpoint"
+ "github.com/TwiN/gatus/v5/test"
+)
+
+func TestAlertProvider_Validate(t *testing.T) {
+ invalidProvider := AlertProvider{DefaultConfig: Config{URL: "", Token: ""}}
+ if err := invalidProvider.Validate(); err == nil {
+ t.Error("provider shouldn't have been valid")
+ }
+ invalidProviderNoToken := AlertProvider{DefaultConfig: Config{URL: "http://homeassistant:8123", Token: ""}}
+ if err := invalidProviderNoToken.Validate(); err == nil {
+ t.Error("provider shouldn't have been valid")
+ }
+ validProvider := AlertProvider{DefaultConfig: Config{URL: "http://homeassistant:8123", Token: "token"}}
+ if err := validProvider.Validate(); err != nil {
+ t.Error("provider should've been valid")
+ }
+}
+
+func TestAlertProvider_ValidateWithOverride(t *testing.T) {
+ providerWithInvalidOverrideGroup := AlertProvider{
+ Overrides: []Override{
+ {
+ Config: Config{URL: "http://homeassistant:8123", Token: "token"},
+ Group: "",
+ },
+ },
+ }
+ if err := providerWithInvalidOverrideGroup.Validate(); err == nil {
+ t.Error("provider Group shouldn't have been valid")
+ }
+ providerWithValidOverride := AlertProvider{
+ DefaultConfig: Config{URL: "http://homeassistant:8123", Token: "token"},
+ Overrides: []Override{
+ {
+ Config: Config{URL: "http://homeassistant:8123", Token: "token"},
+ Group: "group",
+ },
+ },
+ }
+ if err := providerWithValidOverride.Validate(); err != nil {
+ t.Error("provider should've been valid")
+ }
+}
+
+func TestAlertProvider_Send(t *testing.T) {
+ defer client.InjectHTTPClient(nil)
+ firstDescription := "description-1"
+ secondDescription := "description-2"
+ scenarios := []struct {
+ Name string
+ Provider AlertProvider
+ Alert alert.Alert
+ Resolved bool
+ MockRoundTripper test.MockRoundTripper
+ ExpectedError bool
+ }{
+ {
+ Name: "triggered",
+ Provider: AlertProvider{DefaultConfig: Config{URL: "http://homeassistant:8123", Token: "token"}},
+ Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
+ return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
+ }),
+ ExpectedError: false,
+ },
+ {
+ Name: "triggered-error",
+ Provider: AlertProvider{DefaultConfig: Config{URL: "http://homeassistant:8123", Token: "token"}},
+ Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: false,
+ MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
+ return &http.Response{StatusCode: http.StatusInternalServerError, Body: http.NoBody}
+ }),
+ ExpectedError: true,
+ },
+ {
+ Name: "resolved",
+ Provider: AlertProvider{DefaultConfig: Config{URL: "http://homeassistant:8123", Token: "token"}},
+ Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
+ Resolved: true,
+ MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
+ return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
+ }),
+ ExpectedError: false,
+ },
+ }
+ for _, scenario := range scenarios {
+ t.Run(scenario.Name, func(t *testing.T) {
+ client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
+ err := scenario.Provider.Send(
+ &endpoint.Endpoint{Name: "endpoint-name"},
+ &scenario.Alert,
+ &endpoint.Result{
+ ConditionResults: []*endpoint.ConditionResult{
+ {Condition: "SUCCESSFUL_CONDITION", Success: true},
+ {Condition: "FAILING_CONDITION", Success: false},
+ },
+ },
+ scenario.Resolved,
+ )
+ if scenario.ExpectedError && err == nil {
+ t.Error("expected error, got none")
+ }
+ if !scenario.ExpectedError && err != nil {
+ t.Error("expected no error, got", err.Error())
+ }
+ })
+ }
+}
+
+func TestAlertProvider_buildRequestBody(t *testing.T) {
+ description := "test-description"
+ provider := AlertProvider{DefaultConfig: Config{URL: "http://homeassistant:8123", Token: "token"}}
+ body := provider.buildRequestBody(
+ &endpoint.Endpoint{Name: "endpoint-name"},
+ &alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
+ &endpoint.Result{
+ ConditionResults: []*endpoint.ConditionResult{
+ {Condition: "SUCCESSFUL_CONDITION", Success: true},
+ {Condition: "FAILING_CONDITION", Success: false},
+ },
+ },
+ false,
+ )
+ var decodedBody Body
+ if err := json.Unmarshal(body, &decodedBody); err != nil {
+ t.Error("expected body to be valid JSON, got error:", err.Error())
+ }
+ if decodedBody.EventType != "gatus_alert" {
+ t.Errorf("expected event_type to be gatus_alert, got %s", decodedBody.EventType)
+ }
+ if decodedBody.EventData.Status != "triggered" {
+ t.Errorf("expected status to be triggered, got %s", decodedBody.EventData.Status)
+ }
+ if decodedBody.EventData.Description != description {
+ t.Errorf("expected description to be %s, got %s", description, decodedBody.EventData.Description)
+ }
+ if len(decodedBody.EventData.Conditions) != 2 {
+ t.Errorf("expected 2 conditions, got %d", len(decodedBody.EventData.Conditions))
+ }
+ if !decodedBody.EventData.Conditions[0].Success {
+ t.Error("expected first condition to be successful")
+ }
+ if decodedBody.EventData.Conditions[1].Success {
+ t.Error("expected second condition to be unsuccessful")
+ }
+}
diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go
index 94e600f0..b1c3bfaa 100644
--- a/alerting/provider/provider.go
+++ b/alerting/provider/provider.go
@@ -10,6 +10,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/provider/github"
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
+ "github.com/TwiN/gatus/v5/alerting/provider/homeassistant"
"github.com/TwiN/gatus/v5/alerting/provider/incidentio"
"github.com/TwiN/gatus/v5/alerting/provider/jetbrainsspace"
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
@@ -80,6 +81,7 @@ var (
_ AlertProvider = (*github.AlertProvider)(nil)
_ AlertProvider = (*gitlab.AlertProvider)(nil)
_ AlertProvider = (*googlechat.AlertProvider)(nil)
+ _ AlertProvider = (*homeassistant.AlertProvider)(nil)
_ AlertProvider = (*jetbrainsspace.AlertProvider)(nil)
_ AlertProvider = (*matrix.AlertProvider)(nil)
_ AlertProvider = (*mattermost.AlertProvider)(nil)
@@ -105,6 +107,7 @@ var (
_ Config[github.Config] = (*github.Config)(nil)
_ Config[gitlab.Config] = (*gitlab.Config)(nil)
_ Config[googlechat.Config] = (*googlechat.Config)(nil)
+ _ Config[homeassistant.Config] = (*homeassistant.Config)(nil)
_ Config[incidentio.Config] = (*incidentio.Config)(nil)
_ Config[jetbrainsspace.Config] = (*jetbrainsspace.Config)(nil)
_ Config[matrix.Config] = (*matrix.Config)(nil)
diff --git a/config/config.go b/config/config.go
index 701dd84e..e31a4225 100644
--- a/config/config.go
+++ b/config/config.go
@@ -411,6 +411,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi
alert.TypeGitea,
alert.TypeGoogleChat,
alert.TypeGotify,
+ alert.TypeHomeAssistant,
alert.TypeJetBrainsSpace,
alert.TypeMatrix,
alert.TypeMattermost,