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.

282 lines
6.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
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. subDomain := deviceModel.DriverProperties["subDomain"].(string)
  80. if b.luciferMillIDMap[deviceModel.ID] == 0 {
  81. millID, _ := strconv.Atoi(deviceModel.InternalID)
  82. b.luciferMillIDMap[deviceModel.ID] = millID
  83. b.millLuciferIDMap[millID] = deviceModel.ID
  84. }
  85. b.mu.Unlock()
  86. status := 0
  87. if deviceModel.State.Power {
  88. status = 1
  89. }
  90. if subDomainIsGen2(subDomain) {
  91. powerReq := deviceControlReqBody{
  92. SubDomain: subDomain,
  93. DeviceID: b.luciferMillIDMap[deviceModel.ID],
  94. TestStatus: 1,
  95. Status: status,
  96. }
  97. err := b.command(ctx, "deviceControl", powerReq, nil)
  98. if err != nil {
  99. return err
  100. }
  101. tempReq := changeInfoReqBody{
  102. DeviceID: b.luciferMillIDMap[deviceModel.ID],
  103. Value: deviceModel.State.Temperature,
  104. TimeZoneNum: "+02:00",
  105. Key: "holidayTemp",
  106. }
  107. err = b.command(ctx, "changeDeviceInfo", tempReq, nil)
  108. if err != nil {
  109. return err
  110. }
  111. } else {
  112. sd, _ := strconv.Atoi(subDomain)
  113. tempReq := deviceControlGen3Body{
  114. Operation: "SINGLE_CONTROL",
  115. Status: status,
  116. SubDomain: sd,
  117. DeviceId: b.luciferMillIDMap[deviceModel.ID],
  118. HoldTemp: deviceModel.State.Temperature,
  119. }
  120. err := b.command(ctx, "deviceControlGen3ForApp", tempReq, nil)
  121. if err != nil {
  122. return err
  123. }
  124. powerReq := deviceControlGen3Body{
  125. Operation: "SWITCH",
  126. Status: status,
  127. SubDomain: sd,
  128. DeviceId: b.luciferMillIDMap[deviceModel.ID],
  129. }
  130. err = b.command(ctx, "deviceControlGen3ForApp", powerReq, nil)
  131. if err != nil {
  132. return err
  133. }
  134. }
  135. return nil
  136. }
  137. func (b *bridge) command(ctx context.Context, command string, payload interface{}, target interface{}) error {
  138. err := b.authenticate(ctx)
  139. if err != nil {
  140. return err
  141. }
  142. url := serviceEndpoint + command
  143. method := "POST"
  144. nonce := makeNonce()
  145. timestamp := fmt.Sprintf("%d", time.Now().Unix())
  146. timeout := "300"
  147. h := sha1.New()
  148. h.Write([]byte(timeout))
  149. h.Write([]byte(timestamp))
  150. h.Write([]byte(nonce))
  151. h.Write([]byte(b.token))
  152. signature := fmt.Sprintf("%x", h.Sum(nil))
  153. body, err := json.Marshal(payload)
  154. if err != nil {
  155. return err
  156. }
  157. req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
  158. if err != nil {
  159. return err
  160. }
  161. addDefaultHeaders(req)
  162. req.Header.Add("X-Zc-Timestamp", timestamp)
  163. req.Header.Add("X-Zc-Timeout", timeout)
  164. req.Header.Add("X-Zc-Nonce", nonce)
  165. req.Header.Add("X-Zc-User-Id", fmt.Sprintf("%d", b.userId))
  166. req.Header.Add("X-Zc-User-Signature", signature)
  167. req.Header.Add("X-Zc-Content-Length", fmt.Sprintf("%d", len(body)))
  168. res, err := http.DefaultClient.Do(req)
  169. if err != nil {
  170. return lerrors.ErrCannotForwardRequest
  171. } else if res.StatusCode != 200 {
  172. return lerrors.ErrIncorrectToken
  173. }
  174. if target == nil {
  175. return nil
  176. }
  177. err = json.NewDecoder(res.Body).Decode(&target)
  178. if err != nil {
  179. return lerrors.ErrUnexpectedResponse
  180. }
  181. return nil
  182. }
  183. func (b *bridge) authenticate(ctx context.Context) error {
  184. b.mu.Lock()
  185. defer b.mu.Unlock()
  186. if b.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) {
  187. body, err := json.Marshal(authReqBody{
  188. Account: b.username,
  189. Password: b.password,
  190. })
  191. if err != nil {
  192. return lerrors.ErrMissingToken
  193. }
  194. req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint+"login", bytes.NewReader(body))
  195. if err != nil {
  196. return lerrors.ErrMissingToken
  197. }
  198. addDefaultHeaders(req)
  199. res, err := http.DefaultClient.Do(req)
  200. if err != nil {
  201. return lerrors.ErrCannotForwardRequest
  202. } else if res.StatusCode != 200 {
  203. return lerrors.ErrIncorrectToken
  204. }
  205. var resBody authResBody
  206. err = json.NewDecoder(res.Body).Decode(&resBody)
  207. if err != nil {
  208. return lerrors.ErrBridgeSearchFailed
  209. }
  210. log.Printf("Mill: Authenticated as %s", resBody.NickName)
  211. b.userId = resBody.UserID
  212. b.token = resBody.Token
  213. b.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location)
  214. }
  215. return nil
  216. }
  217. func makeNonce() string {
  218. buf := make([]byte, 8)
  219. _, _ = io.ReadFull(rand.Reader, buf)
  220. return fmt.Sprintf("%x", buf)
  221. }
  222. func addDefaultHeaders(req *http.Request) {
  223. req.Header.Add("Content-Type", "application/x-zc-object")
  224. req.Header.Add("Connection", "Keep-Alive")
  225. req.Header.Add("X-Zc-Major-Domain", "seanywell")
  226. req.Header.Add("X-Zc-Msg-Name", "millService")
  227. req.Header.Add("X-Zc-Sub-Domain", "milltype")
  228. req.Header.Add("X-Zc-Seq-Id", "1")
  229. req.Header.Add("X-Zc-Version", "1")
  230. }
  231. var gen2subDomains = []string{"863", "5316", "5317", "5332", "5333", "6933"}
  232. func subDomainIsGen2(subDomain string) bool {
  233. for _, gen2sd := range gen2subDomains {
  234. if subDomain == gen2sd {
  235. return true
  236. }
  237. }
  238. return false
  239. }