现有方案
现在网络上有两个方案,一个是基于Home Assistant + MQTT + NodeRed 来实现的,比如https://bxk64web49.feishu.cn/docx/ 。另外一个就是轮询小米的API,然后在通过tts播放结果,比如https://github.com/yihong0618/gitblog/issues/258
开搞
参考第一个方案,利用现有的群晖系统,在Docker上安装Home Assistant,这样就可以和小爱音箱集成。另外一边,我需要NodeRed来调用ChatGPT。中间我用MQTT来通信。
安装Docker
群晖上安装Docker套件
Docker上安装homeassistant
HA有4种安装模式,树莓派用的是OS的模式,群晖只有Docker模式。可以参考 https://post.smzdm.com/p/az370qk5/ 安装Home Assistant
Docker上安装HACS和MQTT
参考 https://post.smzdm.com/p/a6d57z0n/ 。
安装完了之后,Xiaomi Miot Auto插件,绑定小米设备。
Docker上安装NodeRed
参考 https://post.smzdm.com/p/a9g4r4me/ ,只需要前面安装部分,企业微信机器就不需要了。
NodeRed上需要安装node-red-contrib-home-assistant-websocket
和 node-red-contrib-xiaoai-tts
。最后如下图,NodeRed会通过HomeAssistant监听小爱音箱的请求,并写入MQTT。会有额外一个服务调用ChatGPT并写回MQTT,NodeRed再监听这个topic,通过tts播放出来。
调用ChatGPT
用Golang简单写了一个监听MQTT和调用ChatGPT的服务。用Golang的好处就是,编译出来的文件很小,直接放在很小的image里面就可以了,代码如下。
package main
import (
"os"
"os/signal"
"syscall"
"bytes"
"encoding/json"
"log"
"net/http"
"time"
"fmt"
"regexp"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
const (
MQTTBroker = "tcp://192.168.51.86:1883"
MQTTUsername = ""
MQTTPassword = ""
MQTTClientId = "chatgpt_client"
MQTTTopic = "/xiaomi/chatgpt"
APIEndpoint = "https://api.openai.com/v1/chat/completions"
APIToken = "CHATGPT-TOKEN"
)
type ChatGptApiResponse struct {
Choices []struct {
FinishReason string `json:"finish_reason"`
Index int `json:"index"`
Message struct {
Content string `json:"content"`
Role string `json:"role"`
} `json:"message"`
} `json:"choices"`
Created int64 `json:"created"`
ID string `json:"id"`
Model string `json:"model"`
Object string `json:"object"`
Usage struct {
CompletionTokens int `json:"completion_tokens"`
PromptTokens int `json:"prompt_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) {
fmt.Printf("Connected\n")
client.Subscribe(MQTTTopic, 0, func(client mqtt.Client, msg mqtt.Message) {
processMessage(msg.Payload())
})
}
var connectLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {
fmt.Printf("Connect lost: %+v\n", err)
}
func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
opts := mqtt.NewClientOptions()
opts.AddBroker(MQTTBroker)
opts.SetClientID(MQTTClientId)
opts.SetUsername(MQTTUsername)
opts.SetPassword(MQTTPassword)
opts.OnConnect = connectHandler
opts.OnConnectionLost = connectLostHandler
opts.SetReconnectingHandler(func(c mqtt.Client, options *mqtt.ClientOptions) {
fmt.Printf("...... mqtt reconnecting ......\n")
})
client := mqtt.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
log.Println("Error connecting to MQTT broker:", token.Error())
panic(token.Error())
}
<-c
}
func processMessage(message []byte) {
log.Printf("Received message: %s from topic: %s\n", message, MQTTTopic)
re := regexp.MustCompile("(.*机器人)")
cleanedMessage := re.ReplaceAllString(string(message), "")
apiResponse, err := postToAPI(cleanedMessage)
if err != nil {
log.Println("Error posting message to API:", err)
return
}
err = sendToMQTT(apiResponse.Choices[0].Message.Content, "/xiaomi/reply")
if err != nil {
log.Println("Error sending processed message to MQTT:", err)
return
}
}
func postToAPI(message string) (*ChatGptApiResponse, error) {
// Call API with POST method
apiURL := APIEndpoint
payload := []byte(fmt.Sprintf(`{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "%s"}]}`, message))
log.Printf("calling ChatGPT api for message: %s\n", payload)
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(payload))
if err != nil {
log.Printf("Error creating request: %v\n", err)
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", APIToken))
httpClient := &http.Client{Timeout: time.Second * 30}
resp, err := httpClient.Do(req)
if err != nil {
log.Printf("Error sending request: %v\n", err)
return nil, err
}
defer resp.Body.Close()
// Parse the API response
responseMessage := ChatGptApiResponse{}
err = json.NewDecoder(resp.Body).Decode(&responseMessage)
if err != nil {
log.Printf("Error parsing response body: %v\n", err)
return nil, err
}
log.Printf("API response: %#v\n", responseMessage)
return &responseMessage, nil
}
func sendToMQTT(message string, topic string) error {
opts := mqtt.NewClientOptions()
opts.AddBroker(MQTTBroker)
opts.SetClientID(MQTTClientId)
opts.SetUsername(MQTTUsername)
opts.SetPassword(MQTTPassword)
client := mqtt.NewClient(opts)
if token := client.Connect(); token.Wait() && token.Error() != nil {
return token.Error()
}
defer client.Disconnect(250)
token := client.Publish(topic, 0, false, message)
token.Wait()
return token.Error()
}
这是Dockerfile。Image打完以后,通过docker image save
和docker image load
导入到群晖的Docker里面,也可以通过界面导入。
FROM golang:1.19 as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /mqtt-go
FROM gcr.io/distroless/static-debian11
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
COPY --from=builder /mqtt-go /
CMD ["/mqtt-go"]
小爱开放平台
还有人自己利用自己的服务器,搭配小米开放平台。 https://hgl2.com/2022/homeassis-work-with-xiaomi-ai/