505 lines
15 KiB
Go
505 lines
15 KiB
Go
// Package device 提供设备控制相关功能的控制器
|
||
// 实现设备控制、查询等操作
|
||
package device
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strconv"
|
||
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/api/middleware"
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/controller"
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/logs"
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/model"
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/service"
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/storage/repository"
|
||
"git.huangwc.com/pig/pig-farm-controller/internal/websocket"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ListResponse 设备列表响应结构体
|
||
type ListResponse struct {
|
||
Devices []DeviceListItem `json:"devices"`
|
||
}
|
||
|
||
// DeviceListItem 设备列表项结构体
|
||
type DeviceListItem struct {
|
||
model.Device
|
||
Active bool `json:"active"`
|
||
}
|
||
|
||
// DeviceRequest 设备创建/更新请求结构体
|
||
type DeviceRequest struct {
|
||
Name string `json:"name" binding:"required"` // 设备名称,必填
|
||
Type model.DeviceType `json:"type" binding:"required"` // 设备类型,必填
|
||
ParentID *uint `json:"parent_id,omitempty"` // 父设备ID,可选
|
||
Address *string `json:"address,omitempty"` // 设备地址,可选
|
||
|
||
// 485总线设备的额外字段
|
||
BusNumber *int `json:"bus_number,omitempty"` // 485总线号
|
||
DeviceAddress *string `json:"device_address,omitempty"` // 485设备地址
|
||
}
|
||
|
||
// BindAndValidate 绑定并验证请求数据
|
||
func (req *DeviceRequest) BindAndValidate(data []byte) error {
|
||
// 创建一个map来解析原始JSON
|
||
raw := make(map[string]interface{})
|
||
if err := json.Unmarshal(data, &raw); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 解析已知字段
|
||
if name, ok := raw["name"].(string); ok {
|
||
req.Name = name
|
||
}
|
||
|
||
if typ, ok := raw["type"].(string); ok {
|
||
req.Type = model.DeviceType(typ)
|
||
}
|
||
|
||
// 特殊处理parent_id字段
|
||
if parentIDVal, exists := raw["parent_id"]; exists && parentIDVal != nil {
|
||
switch v := parentIDVal.(type) {
|
||
case float64:
|
||
// JSON数字默认是float64类型
|
||
if v >= 0 {
|
||
parentID := uint(v)
|
||
req.ParentID = &parentID
|
||
}
|
||
case string:
|
||
// 如果是字符串,尝试转换为uint
|
||
if v != "" && v != "null" {
|
||
if parentID, err := strconv.ParseUint(v, 10, 32); err == nil {
|
||
parentIDUint := uint(parentID)
|
||
req.ParentID = &parentIDUint
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 特殊处理address字段
|
||
if addressVal, exists := raw["address"]; exists && addressVal != nil {
|
||
switch v := addressVal.(type) {
|
||
case string:
|
||
// 如果是字符串,直接赋值
|
||
if v != "" {
|
||
req.Address = &v
|
||
}
|
||
}
|
||
}
|
||
|
||
// 特殊处理bus_number字段
|
||
if busNumberVal, exists := raw["bus_number"]; exists && busNumberVal != nil {
|
||
switch v := busNumberVal.(type) {
|
||
case float64:
|
||
// JSON数字默认是float64类型
|
||
busNumber := int(v)
|
||
req.BusNumber = &busNumber
|
||
case string:
|
||
// 如果是字符串,尝试转换为int
|
||
if v != "" && v != "null" {
|
||
if busNumber, err := strconv.Atoi(v); err == nil {
|
||
req.BusNumber = &busNumber
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 特殊处理device_address字段
|
||
if deviceAddressVal, exists := raw["device_address"]; exists && deviceAddressVal != nil {
|
||
switch v := deviceAddressVal.(type) {
|
||
case string:
|
||
// 如果是字符串,直接赋值
|
||
if v != "" {
|
||
req.DeviceAddress = &v
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Controller 设备控制控制器
|
||
type Controller struct {
|
||
deviceControlRepo repository.DeviceControlRepo
|
||
deviceRepo repository.DeviceRepo
|
||
websocketManager *websocket.Manager
|
||
heartbeatService *service.HeartbeatService
|
||
deviceStatusPool *service.DeviceStatusPool
|
||
logger *logs.Logger
|
||
}
|
||
|
||
// NewController 创建设备控制控制器实例
|
||
func NewController(deviceControlRepo repository.DeviceControlRepo, deviceRepo repository.DeviceRepo, websocketManager *websocket.Manager, heartbeatService *service.HeartbeatService, deviceStatusPool *service.DeviceStatusPool) *Controller {
|
||
return &Controller{
|
||
deviceControlRepo: deviceControlRepo,
|
||
deviceRepo: deviceRepo,
|
||
websocketManager: websocketManager,
|
||
heartbeatService: heartbeatService,
|
||
deviceStatusPool: deviceStatusPool,
|
||
logger: logs.NewLogger(),
|
||
}
|
||
}
|
||
|
||
// List 获取设备列表
|
||
func (c *Controller) List(ctx *gin.Context) {
|
||
devices, err := c.deviceRepo.ListAll()
|
||
if err != nil {
|
||
c.logger.Error("获取设备列表失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "获取设备列表失败")
|
||
return
|
||
}
|
||
|
||
// 构建设备列表项,包含设备状态信息
|
||
deviceList := make([]DeviceListItem, len(devices))
|
||
for i, device := range devices {
|
||
// 从设备状态池获取设备状态,默认为非激活状态
|
||
active := false
|
||
if status, exists := c.deviceStatusPool.GetStatus(strconv.FormatUint(uint64(device.ID), 10)); exists {
|
||
active = status.Active
|
||
}
|
||
|
||
deviceList[i] = DeviceListItem{
|
||
Device: device,
|
||
Active: active,
|
||
}
|
||
}
|
||
|
||
controller.SendSuccessResponse(ctx, "获取设备列表成功", ListResponse{Devices: deviceList})
|
||
}
|
||
|
||
// Create 创建设备
|
||
func (c *Controller) Create(ctx *gin.Context) {
|
||
var req DeviceRequest
|
||
|
||
// 直接使用绑定和验证方法处理JSON数据
|
||
rawData, err := ctx.GetRawData()
|
||
if err != nil {
|
||
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "无法读取请求数据: "+err.Error())
|
||
return
|
||
}
|
||
|
||
if err := req.BindAndValidate(rawData); err != nil {
|
||
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "请求参数错误: "+err.Error())
|
||
return
|
||
}
|
||
|
||
device := &model.Device{
|
||
Name: req.Name,
|
||
Type: req.Type,
|
||
ParentID: req.ParentID,
|
||
Address: req.Address,
|
||
}
|
||
|
||
// 如果是485总线设备且提供了总线号和设备地址,则合并为一个地址
|
||
if (req.Type == model.DeviceTypeFan || req.Type == model.DeviceTypeWaterCurtain) &&
|
||
req.BusNumber != nil && req.DeviceAddress != nil {
|
||
device.Set485Address(*req.BusNumber, *req.DeviceAddress)
|
||
}
|
||
|
||
if err := c.deviceRepo.Create(device); err != nil {
|
||
c.logger.Error("创建设备失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "创建设备失败")
|
||
return
|
||
}
|
||
|
||
// 刷新设备状态
|
||
c.heartbeatService.TriggerManualHeartbeatAsync()
|
||
|
||
controller.SendSuccessResponse(ctx, "创建设备成功", device)
|
||
}
|
||
|
||
// Update 更新设备
|
||
func (c *Controller) Update(ctx *gin.Context) {
|
||
var req struct {
|
||
ID uint `json:"id" binding:"required"`
|
||
DeviceRequest
|
||
}
|
||
|
||
// 直接使用绑定和验证方法处理JSON数据
|
||
rawData, err := ctx.GetRawData()
|
||
if err != nil {
|
||
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "无法读取请求数据: "+err.Error())
|
||
return
|
||
}
|
||
|
||
// 先解析ID
|
||
var raw map[string]interface{}
|
||
if json.Unmarshal(rawData, &raw) == nil {
|
||
if idVal, ok := raw["id"]; ok {
|
||
switch id := idVal.(type) {
|
||
case float64:
|
||
req.ID = uint(id)
|
||
case string:
|
||
if idUint, err := strconv.ParseUint(id, 10, 32); err == nil {
|
||
req.ID = uint(idUint)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 再解析DeviceRequest部分
|
||
if err := req.DeviceRequest.BindAndValidate(rawData); err != nil {
|
||
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "请求参数错误: "+err.Error())
|
||
return
|
||
}
|
||
|
||
device, err := c.deviceRepo.FindByID(req.ID)
|
||
if err != nil {
|
||
c.logger.Error("查找设备失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.NotFoundCode, "设备不存在")
|
||
return
|
||
}
|
||
|
||
device.Name = req.Name
|
||
device.Type = req.Type
|
||
device.ParentID = req.ParentID
|
||
device.Address = req.Address
|
||
// 如果是485总线设备且提供了总线号和设备地址,则合并为一个地址
|
||
if (req.Type == model.DeviceTypeFan || req.Type == model.DeviceTypeWaterCurtain) &&
|
||
req.BusNumber != nil && req.DeviceAddress != nil {
|
||
device.Set485Address(*req.BusNumber, *req.DeviceAddress)
|
||
}
|
||
|
||
if err := c.deviceRepo.Update(device); err != nil {
|
||
c.logger.Error("更新设备失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "更新设备失败")
|
||
return
|
||
}
|
||
|
||
// 刷新设备状态
|
||
c.heartbeatService.TriggerManualHeartbeatAsync()
|
||
|
||
controller.SendSuccessResponse(ctx, "更新设备成功", device)
|
||
}
|
||
|
||
// Delete 删除设备
|
||
func (c *Controller) Delete(ctx *gin.Context) {
|
||
var req struct {
|
||
ID uint `json:"id" binding:"required"`
|
||
}
|
||
|
||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "请求参数错误: "+err.Error())
|
||
return
|
||
}
|
||
|
||
// 先检查设备是否存在
|
||
_, err := c.deviceRepo.FindByID(req.ID)
|
||
if err != nil {
|
||
c.logger.Error("查找设备失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.NotFoundCode, "设备不存在")
|
||
return
|
||
}
|
||
|
||
if err := c.deviceRepo.Delete(req.ID); err != nil {
|
||
c.logger.Error("删除设备失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "删除设备失败")
|
||
return
|
||
}
|
||
|
||
// 刷新设备状态
|
||
c.heartbeatService.TriggerManualHeartbeatAsync()
|
||
|
||
controller.SendSuccessResponse(ctx, "删除设备成功", nil)
|
||
}
|
||
|
||
// SwitchRequest 设备控制请求结构体
|
||
type SwitchRequest struct {
|
||
ParentID *uint `json:"parent_id"` // 区域主控ID
|
||
DeviceType string `json:"device_type" binding:"required,oneof=fan water_curtain"`
|
||
DeviceID string `json:"device_id" binding:"required"`
|
||
Action string `json:"action" binding:"required,oneof=on off"`
|
||
}
|
||
|
||
// SwitchResponseData 设备控制响应数据结构体
|
||
type SwitchResponseData struct {
|
||
DeviceType string `json:"device_type"`
|
||
DeviceID string `json:"device_id"`
|
||
Action string `json:"action"`
|
||
Status string `json:"status"` // 添加状态字段
|
||
Message string `json:"message"` // 添加消息字段
|
||
}
|
||
|
||
// DeviceStatusResponse 设备状态响应结构体
|
||
type DeviceStatusResponse struct {
|
||
DeviceID string `json:"device_id"`
|
||
Active bool `json:"active"`
|
||
}
|
||
|
||
// RelayControlData 发送给中继设备的控制数据结构体
|
||
type RelayControlData struct {
|
||
DeviceType string `json:"device_type"`
|
||
DeviceID string `json:"device_id"`
|
||
Action string `json:"action"`
|
||
}
|
||
|
||
// RelayControlResponseData 中继设备控制响应数据结构体
|
||
type RelayControlResponseData struct {
|
||
Status string `json:"status"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
// Switch 设备控制接口
|
||
func (c *Controller) Switch(ctx *gin.Context) {
|
||
// 从上下文中获取用户信息
|
||
userValue, exists := ctx.Get("user")
|
||
if !exists {
|
||
controller.SendErrorResponse(ctx, controller.UnauthorizedCode, "无法获取用户信息")
|
||
return
|
||
}
|
||
|
||
user, ok := userValue.(*middleware.AuthUser)
|
||
if !ok {
|
||
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "用户信息格式错误")
|
||
return
|
||
}
|
||
|
||
var req SwitchRequest
|
||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "请求参数错误")
|
||
return
|
||
}
|
||
|
||
// 通过WebSocket向中继设备发送控制指令
|
||
controlData := RelayControlData{
|
||
DeviceType: req.DeviceType,
|
||
DeviceID: req.DeviceID,
|
||
Action: req.Action,
|
||
}
|
||
|
||
// 发送指令并等待响应
|
||
response, err := c.websocketManager.SendCommandAndWait("relay-001", "control_device", controlData, 0)
|
||
if err != nil {
|
||
c.logger.Error("通过WebSocket发送设备控制指令失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "设备控制失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
// 解析响应数据
|
||
status := "解析失败"
|
||
message := "消息解析失败"
|
||
var responseData RelayControlResponseData
|
||
if err := response.ParseData(&responseData); err == nil {
|
||
status = responseData.Status
|
||
message = responseData.Message
|
||
}
|
||
|
||
// 创建设备控制记录
|
||
if err := c.createDeviceControlRecord(
|
||
user.ID,
|
||
req.DeviceID,
|
||
req.DeviceType,
|
||
req.Action,
|
||
status,
|
||
message,
|
||
); err != nil {
|
||
c.logger.Error("创建设备控制记录失败: " + err.Error())
|
||
controller.SendErrorResponse(ctx, controller.InternalServerErrorCode, "记录控制历史失败")
|
||
return
|
||
}
|
||
|
||
data := SwitchResponseData{
|
||
DeviceType: req.DeviceType,
|
||
DeviceID: req.DeviceID,
|
||
Action: req.Action,
|
||
Status: status,
|
||
Message: message,
|
||
}
|
||
|
||
// 刷新设备状态
|
||
c.heartbeatService.TriggerManualHeartbeatAsync()
|
||
|
||
controller.SendSuccessResponse(ctx, "设备控制成功", data)
|
||
}
|
||
|
||
// createDeviceControlRecord 创建设备控制记录
|
||
func (c *Controller) createDeviceControlRecord(userID uint, deviceID, deviceType, action, status, result string) error {
|
||
// 获取设备信息
|
||
device, err := c.deviceRepo.FindByIDString(deviceID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 构建位置信息
|
||
var location string
|
||
switch device.Type {
|
||
case model.DeviceTypeRelay:
|
||
// 如果设备本身就是中继设备
|
||
location = device.Name
|
||
case model.DeviceTypePigPenController, model.DeviceTypeFeedMillController:
|
||
// 如果设备本身就是区域主控设备
|
||
if device.ParentID != nil {
|
||
// 获取中继设备
|
||
relayDevice, err := c.deviceRepo.FindByID(*device.ParentID)
|
||
if err != nil {
|
||
location = device.Name
|
||
} else {
|
||
location = relayDevice.Name + " -> " + device.Name
|
||
}
|
||
} else {
|
||
location = device.Name
|
||
}
|
||
default:
|
||
// 如果是普通设备(风机、水帘等)
|
||
if device.ParentID != nil {
|
||
// 获取区域主控设备
|
||
parentDevice, err := c.deviceRepo.FindByID(*device.ParentID)
|
||
if err != nil {
|
||
location = "未知区域"
|
||
} else {
|
||
// 检查区域主控设备是否有上级设备(中继设备)
|
||
if parentDevice.ParentID != nil {
|
||
// 获取中继设备
|
||
relayDevice, err := c.deviceRepo.FindByID(*parentDevice.ParentID)
|
||
if err != nil {
|
||
location = parentDevice.Name
|
||
} else {
|
||
location = relayDevice.Name + " -> " + parentDevice.Name
|
||
}
|
||
} else {
|
||
location = parentDevice.Name
|
||
}
|
||
}
|
||
} else {
|
||
location = "未知区域"
|
||
}
|
||
}
|
||
|
||
control := &model.DeviceControl{
|
||
UserID: userID,
|
||
Location: location,
|
||
DeviceType: model.DeviceType(deviceType),
|
||
DeviceID: deviceID,
|
||
Action: action,
|
||
Status: status,
|
||
Result: result,
|
||
}
|
||
|
||
return c.deviceControlRepo.Create(control)
|
||
}
|
||
|
||
// GetDeviceStatus 获取设备当前状态
|
||
func (c *Controller) GetDeviceStatus(ctx *gin.Context) {
|
||
deviceID := ctx.Query("device_id")
|
||
if deviceID == "" {
|
||
controller.SendErrorResponse(ctx, controller.InvalidParameterCode, "设备ID不能为空")
|
||
return
|
||
}
|
||
|
||
// TODO 需要刷新设备状态吗? 刷新的话这个接口可能会很慢
|
||
|
||
// 从设备状态池中获取设备状态
|
||
status, exists := c.deviceStatusPool.GetStatus(deviceID)
|
||
if !exists {
|
||
controller.SendErrorResponse(ctx, controller.NotFoundCode, "设备状态不存在")
|
||
return
|
||
}
|
||
|
||
response := DeviceStatusResponse{
|
||
DeviceID: deviceID,
|
||
Active: status.Active,
|
||
}
|
||
|
||
controller.SendSuccessResponse(ctx, "获取设备状态成功", response)
|
||
}
|