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.

240 lines
5.6 KiB

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