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.

321 lines
7.3 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
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. "errors"
  9. "fmt"
  10. "git.aiterp.net/lucifer/new-server/internal/lerrors"
  11. "git.aiterp.net/lucifer/new-server/models"
  12. "io"
  13. "log"
  14. "net/http"
  15. "strconv"
  16. "sync"
  17. "time"
  18. )
  19. type bridge struct {
  20. mu sync.Mutex
  21. luciferID int
  22. username string
  23. password string
  24. token string
  25. userId int
  26. mustRefreshBy time.Time
  27. luciferMillIDMap map[int]int
  28. millLuciferIDMap map[int]int
  29. }
  30. func (b *bridge) listDevices(ctx context.Context) ([]models.Device, error) {
  31. err := b.authenticate(ctx)
  32. if err != nil {
  33. return nil, err
  34. }
  35. var shlRes listHomeResBody
  36. err = b.command(ctx, "selectHomeList", listHomeReqBody{}, &shlRes)
  37. if err != nil {
  38. return nil, err
  39. }
  40. devices := make([]millDevice, 0, 16)
  41. for _, home := range shlRes.HomeList {
  42. var gidRes listDeviceResBody
  43. err = b.command(ctx, "getIndependentDevices", listDeviceReqBody{HomeID: home.HomeID}, &gidRes)
  44. if err != nil {
  45. return nil, err
  46. }
  47. devices = append(devices, gidRes.DeviceInfo...)
  48. }
  49. luciferDevices := make([]models.Device, len(devices), len(devices))
  50. for i, device := range devices {
  51. luciferDevices[i] = models.Device{
  52. ID: b.millLuciferIDMap[device.DeviceID],
  53. BridgeID: b.luciferID,
  54. InternalID: fmt.Sprintf("%d", device.DeviceID),
  55. Icon: "heater",
  56. Name: device.DeviceName,
  57. Capabilities: []models.DeviceCapability{models.DCTemperatureControl, models.DCPower},
  58. ButtonNames: nil,
  59. DriverProperties: map[string]interface{}{
  60. "subDomain": fmt.Sprintf("%d", device.SubDomainID),
  61. },
  62. UserProperties: nil,
  63. SceneAssignments: nil,
  64. SceneState: nil,
  65. State: models.DeviceState{
  66. Power: device.PowerStatus > 0,
  67. Temperature: device.HolidayTemp,
  68. },
  69. Tags: nil,
  70. }
  71. }
  72. return luciferDevices, nil
  73. }
  74. func (b *bridge) pushStateChange(ctx context.Context, deviceModel models.Device) error {
  75. if ip, ok := deviceModel.DriverProperties["ip"].(string); ok {
  76. return b.pushWifiStateChange(ctx, ip, deviceModel.State.Temperature)
  77. }
  78. b.mu.Lock()
  79. if b.luciferMillIDMap == nil {
  80. b.luciferMillIDMap = make(map[int]int, 4)
  81. b.millLuciferIDMap = make(map[int]int, 4)
  82. }
  83. subDomain := deviceModel.DriverProperties["subDomain"].(string)
  84. if b.luciferMillIDMap[deviceModel.ID] == 0 {
  85. millID, _ := strconv.Atoi(deviceModel.InternalID)
  86. b.luciferMillIDMap[deviceModel.ID] = millID
  87. b.millLuciferIDMap[millID] = deviceModel.ID
  88. }
  89. b.mu.Unlock()
  90. status := 0
  91. if deviceModel.State.Power {
  92. status = 1
  93. }
  94. if subDomainIsGen2(subDomain) {
  95. powerReq := deviceControlReqBody{
  96. SubDomain: subDomain,
  97. DeviceID: b.luciferMillIDMap[deviceModel.ID],
  98. TestStatus: 1,
  99. Status: status,
  100. }
  101. err := b.command(ctx, "deviceControl", powerReq, nil)
  102. if err != nil {
  103. return err
  104. }
  105. tempReq := changeInfoReqBody{
  106. DeviceID: b.luciferMillIDMap[deviceModel.ID],
  107. Value: deviceModel.State.Temperature,
  108. TimeZoneNum: "+02:00",
  109. Key: "holidayTemp",
  110. }
  111. err = b.command(ctx, "changeDeviceInfo", tempReq, nil)
  112. if err != nil {
  113. return err
  114. }
  115. } else {
  116. sd, _ := strconv.Atoi(subDomain)
  117. tempReq := deviceControlGen3Body{
  118. Operation: "SINGLE_CONTROL",
  119. Status: status,
  120. SubDomain: sd,
  121. DeviceId: b.luciferMillIDMap[deviceModel.ID],
  122. HoldTemp: deviceModel.State.Temperature,
  123. }
  124. err := b.command(ctx, "deviceControlGen3ForApp", tempReq, nil)
  125. if err != nil {
  126. return err
  127. }
  128. powerReq := deviceControlGen3Body{
  129. Operation: "SWITCH",
  130. Status: status,
  131. SubDomain: sd,
  132. DeviceId: b.luciferMillIDMap[deviceModel.ID],
  133. }
  134. err = b.command(ctx, "deviceControlGen3ForApp", powerReq, nil)
  135. if err != nil {
  136. return err
  137. }
  138. }
  139. return nil
  140. }
  141. func (b *bridge) pushWifiStateChange(ctx context.Context, ip string, temperature int) error {
  142. b.mu.Lock()
  143. defer b.mu.Unlock()
  144. data, err := json.Marshal(wifiSetTemperatureBody{
  145. Type: "Normal",
  146. Value: temperature,
  147. })
  148. if err != nil {
  149. return err
  150. }
  151. req, err := http.NewRequestWithContext(
  152. ctx,
  153. "POST",
  154. fmt.Sprintf("http://%s/set-temperature", ip),
  155. bytes.NewReader(data),
  156. )
  157. if err != nil {
  158. return err
  159. }
  160. res, err := http.DefaultClient.Do(req)
  161. if err != nil {
  162. return err
  163. }
  164. if res.StatusCode > 299 {
  165. return errors.New("mill: negative response from " + ip)
  166. }
  167. return nil
  168. }
  169. func (b *bridge) command(ctx context.Context, command string, payload interface{}, target interface{}) error {
  170. err := b.authenticate(ctx)
  171. if err != nil {
  172. return err
  173. }
  174. url := serviceEndpoint + command
  175. method := "POST"
  176. nonce := makeNonce()
  177. timestamp := fmt.Sprintf("%d", time.Now().Unix())
  178. timeout := "300"
  179. h := sha1.New()
  180. h.Write([]byte(timeout))
  181. h.Write([]byte(timestamp))
  182. h.Write([]byte(nonce))
  183. h.Write([]byte(b.token))
  184. signature := fmt.Sprintf("%x", h.Sum(nil))
  185. body, err := json.Marshal(payload)
  186. if err != nil {
  187. return err
  188. }
  189. req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
  190. if err != nil {
  191. return err
  192. }
  193. addDefaultHeaders(req)
  194. req.Header.Add("X-Zc-Timestamp", timestamp)
  195. req.Header.Add("X-Zc-Timeout", timeout)
  196. req.Header.Add("X-Zc-Nonce", nonce)
  197. req.Header.Add("X-Zc-User-Id", fmt.Sprintf("%d", b.userId))
  198. req.Header.Add("X-Zc-User-Signature", signature)
  199. req.Header.Add("X-Zc-Content-Length", fmt.Sprintf("%d", len(body)))
  200. res, err := http.DefaultClient.Do(req)
  201. if err != nil {
  202. return lerrors.ErrCannotForwardRequest
  203. } else if res.StatusCode != 200 {
  204. return lerrors.ErrIncorrectToken
  205. }
  206. if target == nil {
  207. return nil
  208. }
  209. err = json.NewDecoder(res.Body).Decode(&target)
  210. if err != nil {
  211. return lerrors.ErrUnexpectedResponse
  212. }
  213. return nil
  214. }
  215. func (b *bridge) authenticate(ctx context.Context) error {
  216. b.mu.Lock()
  217. defer b.mu.Unlock()
  218. if b.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) {
  219. body, err := json.Marshal(authReqBody{
  220. Account: b.username,
  221. Password: b.password,
  222. })
  223. if err != nil {
  224. return lerrors.ErrMissingToken
  225. }
  226. req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint+"login", bytes.NewReader(body))
  227. if err != nil {
  228. return lerrors.ErrMissingToken
  229. }
  230. addDefaultHeaders(req)
  231. res, err := http.DefaultClient.Do(req)
  232. if err != nil {
  233. return lerrors.ErrCannotForwardRequest
  234. } else if res.StatusCode != 200 {
  235. return lerrors.ErrIncorrectToken
  236. }
  237. var resBody authResBody
  238. err = json.NewDecoder(res.Body).Decode(&resBody)
  239. if err != nil {
  240. return lerrors.ErrBridgeSearchFailed
  241. }
  242. log.Printf("Mill: Authenticated as %s", resBody.NickName)
  243. b.userId = resBody.UserID
  244. b.token = resBody.Token
  245. b.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location)
  246. }
  247. return nil
  248. }
  249. func makeNonce() string {
  250. buf := make([]byte, 8)
  251. _, _ = io.ReadFull(rand.Reader, buf)
  252. return fmt.Sprintf("%x", buf)
  253. }
  254. func addDefaultHeaders(req *http.Request) {
  255. req.Header.Add("Content-Type", "application/x-zc-object")
  256. req.Header.Add("Connection", "Keep-Alive")
  257. req.Header.Add("X-Zc-Major-Domain", "seanywell")
  258. req.Header.Add("X-Zc-Msg-Name", "millService")
  259. req.Header.Add("X-Zc-Sub-Domain", "milltype")
  260. req.Header.Add("X-Zc-Seq-Id", "1")
  261. req.Header.Add("X-Zc-Version", "1")
  262. }
  263. var gen2subDomains = []string{"863", "5316", "5317", "5332", "5333", "6933"}
  264. func subDomainIsGen2(subDomain string) bool {
  265. for _, gen2sd := range gen2subDomains {
  266. if subDomain == gen2sd {
  267. return true
  268. }
  269. }
  270. return false
  271. }