444 lines
13 KiB
Go
444 lines
13 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"silk-server-go/internal/middleware"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// MQTTPublisher MQTT 发布接口(后续 MQTT 模块实现后注入)
|
|
type MQTTPublisher interface {
|
|
Publish(topic string, payload interface{}) error
|
|
}
|
|
|
|
// DeviceCommander 设备命令发送接口(向特定设备发送命令)
|
|
type DeviceCommander interface {
|
|
PublishToDevice(deviceKey string, payload interface{}) error
|
|
GetIRResult(deviceKey string) interface{}
|
|
GetIRLearnedCodes(deviceKey string) []int
|
|
}
|
|
|
|
// mqttPublisher 全局 MQTT 发布者实例(后续 MQTT 模块初始化后赋值)
|
|
var mqttPublisher MQTTPublisher
|
|
|
|
// deviceCommander 全局设备命令发送者(用于 GSTMB1 等设备特定命令)
|
|
var deviceCommander DeviceCommander
|
|
|
|
// SetMQTTPublisher 设置全局 MQTT 发布者(供 main 或 MQTT 模块调用)
|
|
func SetMQTTPublisher(p MQTTPublisher) {
|
|
mqttPublisher = p
|
|
}
|
|
|
|
// SetDeviceCommander 设置全局设备命令发送者
|
|
func SetDeviceCommander(d DeviceCommander) {
|
|
deviceCommander = d
|
|
}
|
|
|
|
// ControlCommand 控制命令
|
|
type ControlCommand struct {
|
|
DeviceKey string `json:"deviceKey"`
|
|
Action string `json:"action"`
|
|
Value interface{} `json:"value"`
|
|
Payload map[string]interface{} `json:"payload"`
|
|
}
|
|
|
|
// RegisterControlRoutes 注册控制命令路由
|
|
func RegisterControlRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
|
perm := middleware.RequirePermission(db, "device:control")
|
|
rg.POST("/control/send", perm, sendControl(db))
|
|
rg.POST("/control/batch", perm, batchControl(db))
|
|
rg.POST("/devices/:id/gstmb1/command", perm, sendGSTMB1Command(db))
|
|
rg.POST("/devices/:id/gstmb1/info", perm, sendGSTMB1Info(db))
|
|
rg.POST("/devices/:id/gstmb1/restart", perm, sendGSTMB1Restart(db))
|
|
rg.POST("/devices/:id/gstmb1/interval", perm, sendGSTMB1Interval(db))
|
|
rg.POST("/devices/:id/plug/on", perm, sendPlugOn(db))
|
|
rg.POST("/devices/:id/plug/off", perm, sendPlugOff(db))
|
|
rg.POST("/devices/:id/plug/statistic", perm, sendPlugStatistic(db))
|
|
rg.POST("/devices/:id/plug/info", perm, sendGSTMB1Info(db)) // 插座 info 命令与传感器相同
|
|
// GSCU1B-4G 红外控制器
|
|
rg.POST("/devices/:id/ir/learn", perm, sendIRLearn(db))
|
|
rg.POST("/devices/:id/ir/emit", perm, sendIREmit(db))
|
|
rg.POST("/devices/:id/ir/cancel", perm, sendIRCancel(db))
|
|
rg.POST("/devices/:id/ir/erase", perm, sendIRErase(db))
|
|
rg.GET("/devices/:id/ir/status", perm, getIRStatus(db))
|
|
}
|
|
|
|
// sendControl 下发单条控制命令(通过 MQTT 发布到 devices/{deviceKey}/cmd 主题)
|
|
func sendControl(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var cmd ControlCommand
|
|
if err := c.ShouldBindJSON(&cmd); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if !requireDeviceKeyAccess(c, db, cmd.DeviceKey) {
|
|
return
|
|
}
|
|
result, err := publishControl(cmd)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// ===== GSPE1B 智能插座控制 =====
|
|
|
|
// sendPlugOn 插座通电 {"type":"event","key":1}
|
|
func sendPlugOn(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "event",
|
|
"key": 1,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendPlugOff 插座断电 {"type":"event","key":0}
|
|
func sendPlugOff(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "event",
|
|
"key": 0,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendPlugStatistic 查询插座电量信息 {"type":"statistic"}
|
|
func sendPlugStatistic(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "statistic",
|
|
"messageId": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// ===== GSCU1B-4G 红外控制器 =====
|
|
|
|
// sendIRLearn 学习红外码 {"type":"infrared","action":"learn","data":{"no":N}}
|
|
func sendIRLearn(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
var body struct {
|
|
No int `json:"no"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil || body.No < 1 || body.No > 248 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no 必须为 1-248 的整数"})
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "infrared",
|
|
"action": "learn",
|
|
"data": map[string]int{"no": body.No},
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendIREmit 发射红外码 {"type":"infrared","action":"emit","data":{"no":N}}
|
|
func sendIREmit(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
var body struct {
|
|
No int `json:"no"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil || body.No < 1 || body.No > 248 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no 必须为 1-248 的整数"})
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "infrared",
|
|
"action": "emit",
|
|
"data": map[string]int{"no": body.No},
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendIRCancel 取消学习 {"type":"infrared","action":"learnCancel"}
|
|
func sendIRCancel(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "infrared",
|
|
"action": "learnCancel",
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendIRErase 擦除全部红外码 {"type":"infrared","action":"erase"}
|
|
func sendIRErase(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "infrared",
|
|
"action": "erase",
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// getIRStatus 查询红外操作结果
|
|
func getIRStatus(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
deviceKey, err := getDeviceKey(db, c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if deviceCommander == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "设备命令服务未初始化"})
|
|
return
|
|
}
|
|
result := deviceCommander.GetIRResult(deviceKey)
|
|
learnedCodes := deviceCommander.GetIRLearnedCodes(deviceKey)
|
|
if result == nil {
|
|
c.JSON(http.StatusOK, gin.H{"hasResult": false, "learnedCodes": learnedCodes})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"hasResult": true, "result": result, "learnedCodes": learnedCodes})
|
|
}
|
|
}
|
|
|
|
// batchControl 批量下发控制命令
|
|
func batchControl(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var body struct {
|
|
Commands []ControlCommand `json:"commands"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
results := make([]gin.H, 0, len(body.Commands))
|
|
for _, cmd := range body.Commands {
|
|
if !requireDeviceKeyAccess(c, db, cmd.DeviceKey) {
|
|
continue
|
|
}
|
|
result, err := publishControl(cmd)
|
|
if err != nil {
|
|
results = append(results, gin.H{"error": err.Error(), "deviceKey": cmd.DeviceKey})
|
|
continue
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
c.JSON(http.StatusOK, results)
|
|
}
|
|
}
|
|
|
|
// publishControl 发布控制命令到 MQTT 主题 devices/{deviceKey}/cmd
|
|
func publishControl(cmd ControlCommand) (gin.H, error) {
|
|
if cmd.DeviceKey == "" || cmd.Action == "" {
|
|
return nil, &controlError{"deviceKey/action required"}
|
|
}
|
|
|
|
topic := "devices/" + cmd.DeviceKey + "/cmd"
|
|
|
|
// 构建消息体:action + value + payload 展开 + ts
|
|
payload := map[string]interface{}{
|
|
"action": cmd.Action,
|
|
"value": cmd.Value,
|
|
}
|
|
for k, v := range cmd.Payload {
|
|
payload[k] = v
|
|
}
|
|
payload["ts"] = time.Now().Format(time.RFC3339)
|
|
|
|
// 通过 MQTT 发布(若未注入则仅记录日志)
|
|
if mqttPublisher != nil {
|
|
if err := mqttPublisher.Publish(topic, payload); err != nil {
|
|
slog.Warn("MQTT 发布失败", "topic", topic, "error", err)
|
|
}
|
|
} else {
|
|
slog.Warn("MQTT 发布者未注入,跳过实际发布", "topic", topic)
|
|
}
|
|
|
|
return gin.H{
|
|
"topic": topic,
|
|
"payload": payload,
|
|
"ok": true,
|
|
}, nil
|
|
}
|
|
|
|
// controlError 控制命令错误
|
|
type controlError struct{ msg string }
|
|
|
|
func (e *controlError) Error() string { return e.msg }
|
|
|
|
// ===== GSTMB1 命令处理 =====
|
|
|
|
// getDeviceKey 从数据库查找设备的 deviceKey
|
|
func getDeviceKey(db *gorm.DB, deviceID string) (string, error) {
|
|
var device struct {
|
|
DeviceKey string `gorm:"column:device_key"`
|
|
}
|
|
if err := db.Table("devices").Where("id = ?", deviceID).First(&device).Error; err != nil {
|
|
return "", fmt.Errorf("设备不存在")
|
|
}
|
|
return device.DeviceKey, nil
|
|
}
|
|
|
|
// publishGSTMB1Command 向 GSTMB1 设备发送命令
|
|
func publishGSTMB1Command(db *gorm.DB, deviceID string, payload map[string]interface{}) (gin.H, error) {
|
|
deviceKey, err := getDeviceKey(db, deviceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if deviceCommander == nil {
|
|
return nil, fmt.Errorf("设备命令服务未初始化")
|
|
}
|
|
if err := deviceCommander.PublishToDevice(deviceKey, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
slog.Info("GSTMB1 命令已发送", "deviceKey", deviceKey, "payload", payload)
|
|
return gin.H{"ok": true, "deviceKey": deviceKey, "payload": payload}, nil
|
|
}
|
|
|
|
// sendGSTMB1Command 发送自定义 GSTMB1 命令(body 原样转发)
|
|
func sendGSTMB1Command(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
var body map[string]interface{}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), body)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendGSTMB1Info 获取设备信息 {"type":"info"}
|
|
func sendGSTMB1Info(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "info",
|
|
"messageId": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendGSTMB1Restart 重启设备 {"type":"setting","system":"restart"}
|
|
func sendGSTMB1Restart(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "setting",
|
|
"system": "restart",
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|
|
|
|
// sendGSTMB1Interval 设置定时上报间隔 {"type":"setting","timerEnable":1,"timerInterval":N}
|
|
func sendGSTMB1Interval(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
|
return
|
|
}
|
|
var body struct {
|
|
Interval int `json:"interval"`
|
|
}
|
|
c.ShouldBindJSON(&body)
|
|
interval := body.Interval
|
|
if interval < 5 {
|
|
interval = 60 // 默认 60 秒
|
|
}
|
|
if interval > 86400 {
|
|
interval = 86400
|
|
}
|
|
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
|
"type": "setting",
|
|
"messageId": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
|
"timerEnable": 1,
|
|
"timerInterval": interval,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
}
|