Skip to content

Commit

Permalink
Implement Home Assistant Discovery
Browse files Browse the repository at this point in the history
The discovery logic is implemented and presents and interface that the modules need to implement in order to get their entities being exported to Home Assistant
  • Loading branch information
albertomontesg committed May 12, 2022
1 parent 5c1a8d4 commit 888f259
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 6 deletions.
35 changes: 30 additions & 5 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"github.com/gaetancollaud/digitalstrom-mqtt/pkg/config"
"github.com/gaetancollaud/digitalstrom-mqtt/pkg/controller/modules"
"github.com/gaetancollaud/digitalstrom-mqtt/pkg/digitalstrom"
"github.com/gaetancollaud/digitalstrom-mqtt/pkg/homeassistant"
"github.com/gaetancollaud/digitalstrom-mqtt/pkg/mqtt"
"github.com/rs/zerolog/log"
)

type Controller struct {
dsClient digitalstrom.Client
mqttClient mqtt.Client
dsClient digitalstrom.Client
mqttClient mqtt.Client
hassDiscovery *homeassistant.HomeAssistantDiscovery

modules map[string]modules.Module
}
Expand All @@ -33,10 +35,16 @@ func NewController(config *config.Config) *Controller {
SetTopicPrefix(config.Mqtt.TopicPrefix).
SetRetain(config.Mqtt.Retain)
mqttClient := mqtt.NewClient(mqttOptions)

hass := homeassistant.NewHomeAssistantDiscovery(
mqttClient,
&config.HomeAssistant)

controller := Controller{
dsClient: dsClient,
mqttClient: mqttClient,
modules: map[string]modules.Module{},
dsClient: dsClient,
mqttClient: mqttClient,
hassDiscovery: hass,
modules: map[string]modules.Module{},
}

for name, builder := range modules.Modules {
Expand All @@ -63,6 +71,23 @@ func (c *Controller) Start() error {
}
}

// Retrieve from all the modules the discovery configs to be exported.
for name, module := range c.modules {
m, ok := module.(homeassistant.HomeAssistantDiscoveryInterface)
if !ok {
continue
}
configs, err := m.GetHomeAssistantEntities()
if err != nil {
return fmt.Errorf("error getting discovery configs from module '%s': %w", name, err)
}
c.hassDiscovery.AddConfigs(configs)
}
// Publishes Home Assistant Discovery messages.
if err := c.hassDiscovery.PublishDiscoveryMessages(); err != nil {
return err
}

return nil
}

Expand Down
126 changes: 126 additions & 0 deletions pkg/homeassistant/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package homeassistant

// Interface to expose the endpoints to update any MQTT config needed by the
// Home Assistant discovery package.
type MqttConfig interface {
// Returns a pointer to the device object for any modification required.
GetDevice() *Device
// Adds a new entry on the list of Availability topics.
AddAvailability(Availability) MqttConfig
// Get name of the entity.
GetName() string
// Set name for the entity.
SetName(string) MqttConfig
}

// Structure that encapsulates the information for the device exposed in
// Home Assistant.
type Device struct {
ConfigurationUrl string `json:"configuration_url"`
Identifiers []string `json:"identifiers"`
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
Name string `json:"name"`
}

// Structure that encapsulates the information to retrieve availability of
// devices and entities.
type Availability struct {
Topic string `json:"topic"`
PayloadAvailable string `json:"payload_available,omitempty"`
PayloadNotAvailable string `json:"payload_not_available,omitempty"`
}

// Base config for all MQTT discovery configs.
type BaseConfig struct {
Device Device `json:"device"`
Name string `json:"name"`
UniqueId string `json:"unique_id"`
Retain bool `json:"retain"`
Availability []Availability `json:"availability"`
AvailabilityMode string `json:"availability_mode"`
QoS int `json:"qos"`
}

// Returns a pointer to the device object.
func (c *BaseConfig) GetDevice() *Device {
return &c.Device
}

// Adds a new entry on the list of Availability topics.
func (c *BaseConfig) AddAvailability(availability Availability) MqttConfig {
c.Availability = append(c.Availability, availability)
return c
}

// Get the name of the entity in the configuration.
func (c *BaseConfig) GetName() string {
return c.Name
}

// Set the name for the entity in the configuration.
func (c *BaseConfig) SetName(name string) MqttConfig {
c.Name = name
return c
}

// Light configuration:
// https://www.home-assistant.io/integrations/light.mqtt/
type LightMqttConfig struct {
BaseConfig
CommandTopic string `json:"command_topic,omitempty"`
StateTopic string `json:"state_topic,omitempty"`
PayloadOn string `json:"payload_on,omitempty"`
PayloadOff string `json:"payload_off,omitempty"`
OnCommandType string `json:"on_command_type,omitempty"`
BrightnessScale int `json:"brigthness_scale,omitempty"`
BrightnessStateTopic string `json:"brightness_state_topic,omitempty"`
BrightnessCommandTopic string `json:"brightness_command_topic,omitempty"`
}

// Cover configuration:
// https://www.home-assistant.io/integrations/cover.mqtt/
type CoverConfig struct {
BaseConfig
StateTopic string `json:"state_topic,omitempty"`
StateClosed string `json:"state_closed,omitempty"`
StateOpen string `json:"state_open,omitempty"`
CommandTopic string `json:"command_topic,omitempty"`
PayloadClose string `json:"payload_close,omitempty"`
PayloadOpen string `json:"payload_open,omitempty"`
PayloadStop string `json:"payload_stop,omitempty"`
PositionTopic string `json:"position_topic,omitempty"`
SetPositionTopic string `json:"set_position_topic,omitempty"`
PositionTemplate string `json:"position_template,omitempty"`
}

// Sensor configuration:
// https://www.home-assistant.io/integrations/sensor.mqtt/
type SensorConfig struct {
BaseConfig
StateTopic string `json:"state_topic,omitempty"`
UnitOfMeasurement string `json:"unit_of_measurement,omitempty"`
DeviceClass string `json:"device_class,omitempty"`
Icon string `json:"icon,omitempty"`
}

// Scene configuration:
// https://www.home-assistant.io/integrations/scene.mqtt/
type SceneConfig struct {
BaseConfig
CommandTopic string `json:"command_topic,omitempty"`
PayloadOn string `json:"payload_on,omitempty"`
Icon string `json:"icon,omitempty"`
EnabledByDefault bool `json:"enabled_by_default,omitempty"`
}

// Device Trigger configuration:
// https://www.home-assistant.io/integrations/device_trigger.mqtt/
type DeviceTriggerConfig struct {
BaseConfig
AutomationType string `json:"automation_type"`
Payload string `json:"payload,omitempty"`
Topic string `json:"topic"`
Type string `json:"type"`
Subtype string `json:"subtype"`
}
97 changes: 97 additions & 0 deletions pkg/homeassistant/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package homeassistant

import (
"encoding/json"
"fmt"
"path"

"github.com/gaetancollaud/digitalstrom-mqtt/pkg/config"
"github.com/gaetancollaud/digitalstrom-mqtt/pkg/mqtt"
"github.com/gaetancollaud/digitalstrom-mqtt/pkg/utils"
)

type Domain string

const (
Sensor Domain = "sensor"
Light Domain = "light"
DeviceAutomation Domain = "device_automation"
Cover Domain = "cover"
)

type DiscoveryConfig struct {
Domain Domain
DeviceId string
ObjectId string
Config MqttConfig
}

type HomeAssistantDiscoveryInterface interface {
// Returns the list of Home Assitant MQTT entities that each module would
// be exporting for discovery.
// This will be run after the method Start is called and therefore it can
// assume that the logic there will be run.
GetHomeAssistantEntities() ([]DiscoveryConfig, error)
}

type HomeAssistantDiscovery struct {
mqttClient mqtt.Client
config *config.ConfigHomeAssistant

discoveryConfigs []DiscoveryConfig
}

func NewHomeAssistantDiscovery(mqttClient mqtt.Client, config *config.ConfigHomeAssistant) *HomeAssistantDiscovery {
return &HomeAssistantDiscovery{
mqttClient: mqttClient,
config: config,
discoveryConfigs: []DiscoveryConfig{},
}
}

func (hass *HomeAssistantDiscovery) AddConfigs(configs []DiscoveryConfig) {
systemAvailability := Availability{
Topic: hass.mqttClient.ServerStatusTopic(),
PayloadAvailable: mqtt.Online,
PayloadNotAvailable: mqtt.Offline,
}
for _, config := range configs {
entityName := config.Config.GetName()
config.Config.
SetName(
utils.RemoveRegexp(
entityName,
hass.config.RemoveRegexpFromName)).
AddAvailability(systemAvailability)
// Update the config with some generic attributes for all
// configurations.
device := config.Config.GetDevice()
device.Manufacturer = "DigitalStrom"
device.ConfigurationUrl = "https://" + hass.config.DigitalStromHost

hass.discoveryConfigs = append(hass.discoveryConfigs, config)
}
}

func (hass *HomeAssistantDiscovery) PublishDiscoveryMessages() error {
if !hass.config.DiscoveryEnabled {
return nil
}

for _, config := range hass.discoveryConfigs {
topic := path.Join(
hass.config.DiscoveryTopicPrefix,
string(config.Domain),
config.DeviceId,
config.ObjectId,
"config")
json, err := json.Marshal(config.Config)
if err != nil {
return fmt.Errorf("error serializing dicovery config to JSON: %w", err)
}
if err := hass.mqttClient.Publish(topic, json); err != nil {
return fmt.Errorf("error publishing discovery message to MQTT: %w", err)
}
}
return nil
}
9 changes: 8 additions & 1 deletion pkg/mqtt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ type Client interface {

// Publishes a message under the prefix topic of DigitalStrom.
Publish(topic string, message interface{}) error

// Subscribe to a topic and calls the given handler when a message is
// received.
Subscribe(topic string, messageHandler mqtt.MessageHandler) error
// Returns the topic used to publish the server status.
ServerStatusTopic() string
}

type client struct {
Expand Down Expand Up @@ -101,6 +104,10 @@ func (c *client) publishServerStatus(message string) error {
return c.Publish(serverStatus, message)
}

func (c *client) ServerStatusTopic() string {
return path.Join(c.options.TopicPrefix, serverStatus)
}

func normalizeForTopicName(item string) string {
output := ""
for i := 0; i < len(item); i++ {
Expand Down

0 comments on commit 888f259

Please sign in to comment.