小爱音箱+ChatGPT

Database and Ruby, Python, History


现有方案

现在网络上有两个方案,一个是基于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-websocketnode-red-contrib-xiaoai-tts。最后如下图,NodeRed会通过HomeAssistant监听小爱音箱的请求,并写入MQTT。会有额外一个服务调用ChatGPT并写回MQTT,NodeRed再监听这个topic,通过tts播放出来。

img

调用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 savedocker 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/