Skip to content
This repository was archived by the owner on Oct 4, 2023. It is now read-only.

Commit f4d8bc4

Browse files
committed
implements homeassistant autodiscovery support
1 parent b60cdd7 commit f4d8bc4

6 files changed

Lines changed: 322 additions & 15 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ Or you can download the release binaries: [here](https://github.com/rainu/samsun
1414
./samsung-remote-mqtt -broker tcp://127.0.0.1:1883 -tv-host TV_HOST_NAME
1515
```
1616

17+
Enable the Homeassitant discovery support
18+
```bash
19+
./samsung-remote-mqtt -broker tcp://127.0.0.1:1883 -tv-host TV_HOST_NAME -home-assistant
20+
```
21+
1722
## Trigger command execution
1823

1924
To execute a command:
@@ -45,4 +50,7 @@ mosquitto_sub -t "cmnd/samsung-remote/info/result"
4550
|%prefix%/app/installed|-|Get a list of all installed apps.|
4651

4752
All actions will send an answer to the corresponding result topic. The result topic is the incoming topic plus "result".
48-
For example: The result of **%prefix%/info** will send to **%prefix%/info/result**
53+
For example: The result of **%prefix%/info** will send to **%prefix%/info/result**
54+
55+
Before and after each action the state will publish to the state topic. The state topic is the incoming topic plus "state".
56+
For example: The state of **%prefix%/info** will send to **%prefix%/info/state**

cmd/samsung-remote-mqtt/config.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ type applicationConfig struct {
1515
Username *string
1616
Password *string
1717
ClientId *string
18-
DeviceName *string
1918
TopicPrefix *string
2019

2120
SamsungTvHost *string
2221
SamsungRemoteName *string
2322
SamsungTokenPath *string
23+
24+
HomeassistantEnable *bool
25+
HomeassistantTopic *string
2426
}
2527

2628
var Config applicationConfig
@@ -33,12 +35,14 @@ func LoadConfig() {
3335
Username: flag.String("user", "", "The User (optional)"),
3436
Password: flag.String("password", "", "The password (optional)"),
3537
ClientId: flag.String("client-id", "samsung-remote", "The ClientID (default samsung-remote)"),
36-
DeviceName: flag.String("device-name", "SamsungRemote", "The name of this remote (default SamsungRemote)"),
3738
TopicPrefix: flag.String("topic-prefix", "cmnd/samsung-remote", "The mqtt topic to listen for incoming commands (default cmnd/samsung-remote)"),
3839

3940
SamsungTvHost: flag.String("tv-host", "", "The samsung tv host address. ex: 192.168.1.123"),
4041
SamsungRemoteName: flag.String("tv-remote-name", "samung-remote-mqtt", "The name of the remote. This name will displayed at the TV. (optional)"),
4142
SamsungTokenPath: flag.String("tv-remote-token-file", "./samsung-remote.token", "The path to the file were the token will be saved for reuse. (optional)"),
43+
44+
HomeassistantEnable: flag.Bool("home-assistant", false, "Enable home assistant support (optional)"),
45+
HomeassistantTopic: flag.String("ha-discovery-prefix", "homeassistant/", "The mqtt topic prefix for homeassistant's discovery (optional)"),
4246
}
4347
flag.Parse()
4448

cmd/samsung-remote-mqtt/main.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
MQTT "github.com/eclipse/paho.mqtt.golang"
66
"github.com/rainu/samsung-remote-mqtt/internal/mqtt"
7+
"github.com/rainu/samsung-remote-mqtt/internal/mqtt/hassio"
78
samsungRemoteHTTP "github.com/rainu/samsung-remote/http"
89
samsungRemoteWS "github.com/rainu/samsung-remote/ws"
910
"go.uber.org/zap"
@@ -62,6 +63,23 @@ func main() {
6263
zap.L().Fatal("Error while connecting to mqtt broker: %s", zap.Error(token.Error()))
6364
}
6465

66+
//if hassio is enabled -> publish the hassio mqtt-discovery configs
67+
if *Config.HomeassistantEnable {
68+
information, err := httpRemote.GetInformation()
69+
if err != nil {
70+
zap.L().Warn("Could not get device information: %s", zap.Error(err))
71+
}
72+
73+
haClient := hassio.Client{
74+
DeviceInfo: information,
75+
RemoteName: *Config.SamsungRemoteName,
76+
TopicPrefix: *Config.TopicPrefix,
77+
HassioTopicPrefix: *Config.HomeassistantTopic,
78+
MqttClient: client,
79+
}
80+
haClient.PublishDiscoveryConfig()
81+
}
82+
6583
executor.Initialise(*Config.TopicPrefix, byte(*Config.SubscribeQOS), byte(*Config.PublishQOS))
6684
statusWorker.Initialise(*Config.TopicPrefix)
6785

internal/mqtt/executor.go

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import (
1313
"strings"
1414
)
1515

16+
const (
17+
PayloadStatusRunning = "RUNNING"
18+
PayloadStatusStopped = "STOPPED"
19+
)
20+
1621
type Executor struct {
1722
initialised bool
1823
subscribeQOS byte
@@ -33,18 +38,18 @@ func (e *Executor) Initialise(topicPrefix string, subscribeQOS, publishQOS byte)
3338
e.publishQOS = publishQOS
3439

3540
e.topics = map[string]MQTT.MessageHandler{
36-
fmt.Sprintf("%s/info", topicPrefix): e.handleInfo,
37-
fmt.Sprintf("%s/send-key", topicPrefix): e.handleSendKey,
38-
fmt.Sprintf("%s/send-text", topicPrefix): e.handleSendText,
39-
fmt.Sprintf("%s/move", topicPrefix): e.handleMove,
40-
fmt.Sprintf("%s/left-click", topicPrefix): e.handleLeftClick,
41-
fmt.Sprintf("%s/right-click", topicPrefix): e.handleRightClick,
42-
fmt.Sprintf("%s/browser", topicPrefix): e.handleBrowser,
43-
fmt.Sprintf("%s/app/start", topicPrefix): e.handleAppStart,
44-
fmt.Sprintf("%s/app/start/+", topicPrefix): e.handleAppStartByName,
45-
fmt.Sprintf("%s/app/stop", topicPrefix): e.handleAppStop,
46-
fmt.Sprintf("%s/app/status", topicPrefix): e.handleAppStatus,
47-
fmt.Sprintf("%s/app/installed", topicPrefix): e.handleAppInstalled,
41+
fmt.Sprintf("%s/info", topicPrefix): e.stateWrapper(e.handleInfo),
42+
fmt.Sprintf("%s/send-key", topicPrefix): e.stateWrapper(e.handleSendKey),
43+
fmt.Sprintf("%s/send-text", topicPrefix): e.stateWrapper(e.handleSendText),
44+
fmt.Sprintf("%s/move", topicPrefix): e.stateWrapper(e.handleMove),
45+
fmt.Sprintf("%s/left-click", topicPrefix): e.stateWrapper(e.handleLeftClick),
46+
fmt.Sprintf("%s/right-click", topicPrefix): e.stateWrapper(e.handleRightClick),
47+
fmt.Sprintf("%s/browser", topicPrefix): e.stateWrapper(e.handleBrowser),
48+
fmt.Sprintf("%s/app/start", topicPrefix): e.stateWrapper(e.handleAppStart),
49+
fmt.Sprintf("%s/app/start/+", topicPrefix): e.stateWrapper(e.handleAppStartByName),
50+
fmt.Sprintf("%s/app/stop", topicPrefix): e.stateWrapper(e.handleAppStop),
51+
fmt.Sprintf("%s/app/status", topicPrefix): e.stateWrapper(e.handleAppStatus),
52+
fmt.Sprintf("%s/app/installed", topicPrefix): e.stateWrapper(e.handleAppInstalled),
4853
}
4954

5055
for topic, handler := range e.topics {
@@ -161,6 +166,18 @@ func (e *Executor) handleAppInstalled(client MQTT.Client, message MQTT.Message)
161166
}
162167
}
163168

169+
func (e *Executor) stateWrapper(delegate MQTT.MessageHandler) MQTT.MessageHandler {
170+
return func(client MQTT.Client, message MQTT.Message) {
171+
e.sendStatus(client, message, []byte(PayloadStatusRunning))
172+
delegate(client, message)
173+
e.sendStatus(client, message, []byte(PayloadStatusStopped))
174+
}
175+
}
176+
177+
func (e *Executor) sendStatus(client MQTT.Client, message MQTT.Message, state []byte) {
178+
client.Publish(fmt.Sprintf("%s/state", message.Topic()), e.publishQOS, false, state)
179+
}
180+
164181
func (e *Executor) sendAnswer(client MQTT.Client, message MQTT.Message, answer []byte, err error) {
165182
if err != nil {
166183
answer = []byte(fmt.Sprintf(`ERROR %s`, err.Error()))

internal/mqtt/hassio/client.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package hassio
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
MQTT "github.com/eclipse/paho.mqtt.golang"
7+
"github.com/rainu/samsung-remote-mqtt/internal/mqtt"
8+
"go.uber.org/zap"
9+
"strings"
10+
)
11+
12+
type generalConfig struct {
13+
Name string `json:"name"`
14+
AvailabilityTopic string `json:"avty_t,omitempty"`
15+
PayloadAvailable string `json:"pl_avail,omitempty"`
16+
PayloadNotAvailable string `json:"pl_not_avail,omitempty"`
17+
UniqueId string `json:"uniq_id"`
18+
Icon string `json:"ic,omitempty"`
19+
Device device `json:"dev,omitempty"`
20+
}
21+
22+
type sensorConfig struct {
23+
generalConfig
24+
25+
StateTopic string `json:"stat_t"`
26+
}
27+
28+
type triggerConfig struct {
29+
generalConfig
30+
31+
CommandTopic string `json:"cmd_t"`
32+
StateTopic string `json:"stat_t"`
33+
PayloadStart string `json:"pl_on"`
34+
PayloadStop string `json:"pl_off"`
35+
StateRunning string `json:"stat_on"`
36+
StateStopped string `json:"stat_off"`
37+
}
38+
39+
type device struct {
40+
Name string `json:"name,omitempty"`
41+
Ids []string `json:"ids"`
42+
Model string `json:"mdl,omitempty"`
43+
Manufacturer string `json:"mf,omitempty"`
44+
Version string `json:"sw,omitempty"`
45+
}
46+
47+
type AvailabilityConfig struct {
48+
Topic string
49+
AvailablePayload string
50+
NotAvailablePayload string
51+
}
52+
53+
type Client struct {
54+
parsedDeviceInfo deviceInfo
55+
DeviceInfo string
56+
RemoteName string
57+
58+
HassioTopicPrefix string
59+
TopicPrefix string
60+
MqttClient MQTT.Client
61+
}
62+
63+
type deviceInfo struct {
64+
Id string `json:"id"`
65+
Name string `json:"name"`
66+
Type string `json:"type"`
67+
Version string `json:"version"`
68+
Device struct {
69+
OS string `json:"OS"`
70+
FirmwareVersion string `json:"firmwareVersion"`
71+
Model string `json:"model"`
72+
ModelName string `json:"modelName"`
73+
Name string `json:"name"`
74+
Resolution string `json:"resolution"`
75+
SSID string `json:"ssid"`
76+
WifiMac string `json:"wifiMac"`
77+
} `json:"device"`
78+
}
79+
80+
func (c *Client) PublishDiscoveryConfig() {
81+
zap.L().Info("Initialise homeassistant config.")
82+
83+
//try to parse raw device information
84+
if err := json.Unmarshal([]byte(c.DeviceInfo), &c.parsedDeviceInfo); err != nil {
85+
zap.L().Debug("Could not parse raw device info: %s", zap.Error(err))
86+
}
87+
88+
targetTopic := fmt.Sprintf("%ssensor/%s_status/config", c.HassioTopicPrefix, c.deviceId())
89+
payload := c.generatePayloadForStatus()
90+
c.MqttClient.Publish(targetTopic, byte(0), false, payload)
91+
92+
for keyName, description := range SamsungRemoteKeys {
93+
for topic, payload := range c.generatePayloadForSendKey(keyName, description) {
94+
c.MqttClient.Publish(topic, byte(0), false, payload)
95+
}
96+
}
97+
}
98+
99+
func (c *Client) generatePayloadForStatus() []byte {
100+
conf := sensorConfig{
101+
generalConfig: generalConfig{
102+
Name: "Status",
103+
PayloadAvailable: mqtt.StatusOnline,
104+
PayloadNotAvailable: mqtt.StatusOffline,
105+
UniqueId: fmt.Sprintf("%s_status", c.deviceId()),
106+
Device: c.buildDevice(),
107+
},
108+
StateTopic: fmt.Sprintf("%s/status", c.TopicPrefix),
109+
}
110+
111+
payload, err := json.Marshal(conf)
112+
if err != nil {
113+
//the "marshalling" is relatively safe - it should never appear at runtime
114+
panic(err)
115+
}
116+
return payload
117+
}
118+
119+
func (c *Client) generatePayloadForSendKey(key, description string) map[string][]byte {
120+
payloads := map[string][]byte{}
121+
var conf interface{}
122+
123+
conf = triggerConfig{
124+
generalConfig: generalConfig{
125+
Name: fmt.Sprintf("%s - Button", description),
126+
Icon: "mdi:remote",
127+
UniqueId: fmt.Sprintf("%s_%s", c.deviceId(), key),
128+
Device: c.buildDevice(),
129+
AvailabilityTopic: fmt.Sprintf("%s/status", c.TopicPrefix),
130+
PayloadAvailable: mqtt.StatusOnline,
131+
PayloadNotAvailable: mqtt.StatusOffline,
132+
},
133+
CommandTopic: fmt.Sprintf("%s/send-key", c.TopicPrefix),
134+
PayloadStart: key,
135+
StateTopic: fmt.Sprintf("%s/send-key/state", c.TopicPrefix),
136+
StateRunning: mqtt.PayloadStatusRunning,
137+
StateStopped: mqtt.PayloadStatusStopped,
138+
}
139+
140+
payload, err := json.Marshal(conf)
141+
if err != nil {
142+
//the "marshalling" is relatively safe - it should never appear at runtime
143+
panic(err)
144+
}
145+
topic := fmt.Sprintf("%sswitch/%s/%s/config", c.HassioTopicPrefix, c.deviceId(), key)
146+
payloads[topic] = payload
147+
148+
conf = sensorConfig{
149+
generalConfig: generalConfig{
150+
Name: fmt.Sprintf("%s - State", description),
151+
UniqueId: fmt.Sprintf("%s_%s_state", c.deviceId(), key),
152+
Device: c.buildDevice(),
153+
AvailabilityTopic: fmt.Sprintf("%s/status", c.TopicPrefix),
154+
PayloadAvailable: mqtt.StatusOnline,
155+
PayloadNotAvailable: mqtt.StatusOffline,
156+
},
157+
StateTopic: fmt.Sprintf("%s/send-key/state", c.TopicPrefix),
158+
}
159+
160+
payload, err = json.Marshal(conf)
161+
if err != nil {
162+
//the "marshalling" is relatively safe - it should never appear at runtime
163+
panic(err)
164+
}
165+
topic = fmt.Sprintf("%ssensor/%s_%s/result/config", c.HassioTopicPrefix, c.deviceId(), key)
166+
payloads[topic] = payload
167+
168+
conf = sensorConfig{
169+
generalConfig: generalConfig{
170+
Name: fmt.Sprintf("%s - Result", description),
171+
UniqueId: fmt.Sprintf("%s_%s_result", c.deviceId(), key),
172+
Device: c.buildDevice(),
173+
AvailabilityTopic: fmt.Sprintf("%s/status", c.TopicPrefix),
174+
PayloadAvailable: mqtt.StatusOnline,
175+
PayloadNotAvailable: mqtt.StatusOffline,
176+
},
177+
StateTopic: fmt.Sprintf("%s/send-key/result", c.TopicPrefix),
178+
}
179+
180+
payload, err = json.Marshal(conf)
181+
if err != nil {
182+
//the "marshalling" is relatively safe - it should never appear at runtime
183+
panic(err)
184+
}
185+
topic = fmt.Sprintf("%ssensor/%s_%s/state/config", c.TopicPrefix, c.deviceId(), key)
186+
payloads[topic] = payload
187+
188+
return payloads
189+
}
190+
191+
func (c *Client) buildDevice() device {
192+
d := device{
193+
Name: c.parsedDeviceInfo.Name,
194+
Ids: []string{c.deviceId()},
195+
Manufacturer: "Samsung",
196+
Model: c.parsedDeviceInfo.Device.ModelName,
197+
Version: c.parsedDeviceInfo.Version,
198+
}
199+
200+
if d.Name == "" {
201+
d.Name = c.RemoteName
202+
}
203+
204+
return d
205+
}
206+
207+
func (c *Client) deviceId() string {
208+
if c.parsedDeviceInfo.Device.WifiMac != "" {
209+
return strings.Replace(c.parsedDeviceInfo.Device.WifiMac, ":", "", -1)
210+
}
211+
if c.parsedDeviceInfo.Id != "" {
212+
return c.parsedDeviceInfo.Id
213+
}
214+
if c.parsedDeviceInfo.Name != "" {
215+
return c.parsedDeviceInfo.Name
216+
}
217+
218+
return c.RemoteName
219+
}

0 commit comments

Comments
 (0)