You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

241 lines
5.6 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. package mill
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/rand"
  6. "crypto/sha1"
  7. "encoding/json"
  8. "fmt"
  9. "git.aiterp.net/lucifer/new-server/internal/lerrors"
  10. "git.aiterp.net/lucifer/new-server/models"
  11. "io"
  12. "log"
  13. "net/http"
  14. "strconv"
  15. "sync"
  16. "time"
  17. )
  18. type bridge struct {
  19. mu sync.Mutex
  20. luciferID int
  21. username string
  22. password string
  23. token string
  24. userId int
  25. mustRefreshBy time.Time
  26. luciferMillIDMap map[int]int
  27. millLuciferIDMap map[int]int
  28. }
  29. func (b *bridge) listDevices(ctx context.Context) ([]models.Device, error) {
  30. err := b.authenticate(ctx)
  31. if err != nil {
  32. return nil, err
  33. }
  34. var shlRes listHomeResBody
  35. err = b.command(ctx, "selectHomeList", listHomeReqBody{}, &shlRes)
  36. if err != nil {
  37. return nil, err
  38. }
  39. devices := make([]millDevice, 0, 16)
  40. for _, home := range shlRes.HomeList {
  41. var gidRes listDeviceResBody
  42. err = b.command(ctx, "getIndependentDevices", listDeviceReqBody{HomeID: home.HomeID}, &gidRes)
  43. if err != nil {
  44. return nil, err
  45. }
  46. devices = append(devices, gidRes.DeviceInfo...)
  47. }
  48. luciferDevices := make([]models.Device, len(devices), len(devices))
  49. for i, device := range devices {
  50. luciferDevices[i] = models.Device{
  51. ID: b.millLuciferIDMap[device.DeviceID],
  52. BridgeID: b.luciferID,
  53. InternalID: fmt.Sprintf("%d", device.DeviceID),
  54. Icon: "heater",
  55. Name: device.DeviceName,
  56. Capabilities: []models.DeviceCapability{models.DCTemperatureControl, models.DCPower},
  57. ButtonNames: nil,
  58. DriverProperties: map[string]interface{}{
  59. "subDomain": fmt.Sprintf("%d", device.SubDomainID),
  60. },
  61. UserProperties: nil,
  62. SceneAssignments: nil,
  63. SceneState: nil,
  64. State: models.DeviceState{
  65. Power: device.PowerStatus > 0,
  66. Temperature: device.HolidayTemp,
  67. },
  68. Tags: nil,
  69. }
  70. }
  71. return luciferDevices, nil
  72. }
  73. func (b *bridge) pushStateChange(ctx context.Context, deviceModel models.Device) error {
  74. b.mu.Lock()
  75. if b.luciferMillIDMap == nil {
  76. b.luciferMillIDMap = make(map[int]int, 4)
  77. b.millLuciferIDMap = make(map[int]int, 4)
  78. }
  79. if b.luciferMillIDMap[deviceModel.ID] == 0 {
  80. millID, _ := strconv.Atoi(deviceModel.InternalID)
  81. b.luciferMillIDMap[deviceModel.ID] = millID
  82. b.millLuciferIDMap[millID] = deviceModel.ID
  83. }
  84. b.mu.Unlock()
  85. status := 0
  86. if deviceModel.State.Power {
  87. status = 1
  88. }
  89. powerReq := deviceControlReqBody{
  90. SubDomain: deviceModel.DriverProperties["subDomain"].(string),
  91. DeviceID: b.luciferMillIDMap[deviceModel.ID],
  92. TestStatus: 1,
  93. Status: status,
  94. }
  95. err := b.command(ctx, "deviceControl", powerReq, nil)
  96. if err != nil {
  97. return err
  98. }
  99. tempReq := changeInfoReqBody{
  100. DeviceID: b.luciferMillIDMap[deviceModel.ID],
  101. Value: deviceModel.State.Temperature,
  102. TimeZoneNum: "+02:00",
  103. Key: "holidayTemp",
  104. }
  105. err = b.command(ctx, "changeDeviceInfo", tempReq, nil)
  106. if err != nil {
  107. return err
  108. }
  109. return nil
  110. }
  111. func (b *bridge) command(ctx context.Context, command string, payload interface{}, target interface{}) error {
  112. err := b.authenticate(ctx)
  113. if err != nil {
  114. return err
  115. }
  116. url := serviceEndpoint + command
  117. method := "POST"
  118. nonce := makeNonce()
  119. timestamp := fmt.Sprintf("%d", time.Now().Unix())
  120. timeout := "300"
  121. h := sha1.New()
  122. h.Write([]byte(timeout))
  123. h.Write([]byte(timestamp))
  124. h.Write([]byte(nonce))
  125. h.Write([]byte(b.token))
  126. signature := fmt.Sprintf("%x", h.Sum(nil))
  127. body, err := json.Marshal(payload)
  128. if err != nil {
  129. return err
  130. }
  131. req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
  132. if err != nil {
  133. return err
  134. }
  135. addDefaultHeaders(req)
  136. req.Header.Add("X-Zc-Timestamp", timestamp)
  137. req.Header.Add("X-Zc-Timeout", timeout)
  138. req.Header.Add("X-Zc-Nonce", nonce)
  139. req.Header.Add("X-Zc-User-Id", fmt.Sprintf("%d", b.userId))
  140. req.Header.Add("X-Zc-User-Signature", signature)
  141. req.Header.Add("X-Zc-Content-Length", fmt.Sprintf("%d", len(body)))
  142. res, err := http.DefaultClient.Do(req)
  143. if err != nil {
  144. return lerrors.ErrCannotForwardRequest
  145. } else if res.StatusCode != 200 {
  146. return lerrors.ErrIncorrectToken
  147. }
  148. if target == nil {
  149. return nil
  150. }
  151. err = json.NewDecoder(res.Body).Decode(&target)
  152. if err != nil {
  153. return lerrors.ErrUnexpectedResponse
  154. }
  155. return nil
  156. }
  157. func (b *bridge) authenticate(ctx context.Context) error {
  158. b.mu.Lock()
  159. defer b.mu.Unlock()
  160. if b.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) {
  161. body, err := json.Marshal(authReqBody{
  162. Account: b.username,
  163. Password: b.password,
  164. })
  165. if err != nil {
  166. return lerrors.ErrMissingToken
  167. }
  168. req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint+"login", bytes.NewReader(body))
  169. if err != nil {
  170. return lerrors.ErrMissingToken
  171. }
  172. addDefaultHeaders(req)
  173. res, err := http.DefaultClient.Do(req)
  174. if err != nil {
  175. return lerrors.ErrCannotForwardRequest
  176. } else if res.StatusCode != 200 {
  177. return lerrors.ErrIncorrectToken
  178. }
  179. var resBody authResBody
  180. err = json.NewDecoder(res.Body).Decode(&resBody)
  181. if err != nil {
  182. return lerrors.ErrBridgeSearchFailed
  183. }
  184. log.Printf("Mill: Authenticated as %s", resBody.NickName)
  185. b.userId = resBody.UserID
  186. b.token = resBody.Token
  187. b.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location)
  188. }
  189. return nil
  190. }
  191. func makeNonce() string {
  192. buf := make([]byte, 8)
  193. _, _ = io.ReadFull(rand.Reader, buf)
  194. return fmt.Sprintf("%x", buf)
  195. }
  196. func addDefaultHeaders(req *http.Request) {
  197. req.Header.Add("Content-Type", "application/x-zc-object")
  198. req.Header.Add("Connection", "Keep-Alive")
  199. req.Header.Add("X-Zc-Major-Domain", "seanywell")
  200. req.Header.Add("X-Zc-Msg-Name", "millService")
  201. req.Header.Add("X-Zc-Sub-Domain", "milltype")
  202. req.Header.Add("X-Zc-Seq-Id", "1")
  203. req.Header.Add("X-Zc-Version", "1")
  204. }