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.

367 lines
8.0 KiB

  1. package mill
  2. import (
  3. "bytes"
  4. "crypto/sha1"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. lucifer3 "git.aiterp.net/lucifer3/server"
  9. "git.aiterp.net/lucifer3/server/device"
  10. "git.aiterp.net/lucifer3/server/events"
  11. "git.aiterp.net/lucifer3/server/internal/gentools"
  12. "log"
  13. "math"
  14. "net/http"
  15. "strconv"
  16. "sync"
  17. "time"
  18. )
  19. type OnlineBridge struct {
  20. ID string
  21. Email string
  22. Password string
  23. Gen2 bool
  24. Gen3 bool
  25. genMap map[string]int
  26. internalMap map[string]int
  27. nameMap map[string]string
  28. stateMap map[string]device.State
  29. mx sync.Mutex
  30. started bool
  31. bus *lucifer3.EventBus
  32. token string
  33. userId int
  34. mustRefreshBy time.Time
  35. }
  36. func (o *OnlineBridge) SetBus(bus *lucifer3.EventBus) {
  37. o.bus = bus
  38. }
  39. func (o *OnlineBridge) SetState(id string, state device.State) bool {
  40. if !o.started {
  41. return false
  42. }
  43. if err := o.authenticate(); err != nil {
  44. o.bus.RunEvent(deviceFailed(o.ID, err))
  45. return false
  46. }
  47. currState := o.stateMap[id]
  48. if state.Power != nil {
  49. currState.Power = state.Power
  50. }
  51. if state.Temperature != nil {
  52. currState.Temperature = state.Temperature
  53. }
  54. o.stateMap[id] = currState
  55. if err := o.refresh(); err != nil {
  56. o.bus.RunEvent(deviceFailed(o.ID, err))
  57. return false
  58. }
  59. return true
  60. }
  61. func (o *OnlineBridge) Start() {
  62. o.genMap = make(map[string]int, 16)
  63. o.internalMap = make(map[string]int, 16)
  64. o.nameMap = make(map[string]string, 16)
  65. o.stateMap = make(map[string]device.State, 16)
  66. if err := o.refresh(); err != nil {
  67. o.bus.RunEvent(deviceFailed(o.ID, err))
  68. return
  69. }
  70. o.started = true
  71. go func() {
  72. defer func() {
  73. recover()
  74. log.Printf("Mill: %s stopped unexpectedly", o.ID)
  75. o.started = false
  76. }()
  77. for {
  78. time.Sleep(5 * time.Minute)
  79. if err := o.refresh(); err != nil {
  80. o.bus.RunEvent(deviceFailed(o.ID, err))
  81. break
  82. }
  83. }
  84. }()
  85. }
  86. func (o *OnlineBridge) IsStarted() bool {
  87. return o.started
  88. }
  89. func (o *OnlineBridge) refresh() error {
  90. var shlRes listHomeResBody
  91. err := o.command("selectHomeList", listHomeReqBody{}, &shlRes)
  92. if err != nil {
  93. return err
  94. }
  95. devices := make([]millDevice, 0, 16)
  96. for _, home := range shlRes.HomeList {
  97. var gidRes listDeviceResBody
  98. err = o.command("getIndependentDevices", listDeviceReqBody{HomeID: home.HomeID}, &gidRes)
  99. if err != nil {
  100. return err
  101. }
  102. devices = append(devices, gidRes.DeviceInfo...)
  103. }
  104. for _, mDevice := range devices {
  105. id := fmt.Sprintf("%s:%d", o.ID, mDevice.DeviceID)
  106. if o.internalMap[id] == 0 {
  107. o.genMap[id] = subDomainToGeneration(mDevice.SubDomainID)
  108. o.internalMap[id] = mDevice.DeviceID
  109. o.nameMap[id] = mDevice.DeviceName
  110. o.stateMap[id] = device.State{
  111. Power: gentools.Ptr(mDevice.PowerStatus > 0),
  112. Temperature: gentools.Ptr(float64(mDevice.HolidayTemp)),
  113. }
  114. // Only register devices if generation is configured
  115. if o.allowsGeneration(o.genMap[id]) {
  116. o.bus.RunEvent(events.DeviceReady{ID: id})
  117. o.bus.RunEvent(events.HardwareMetadata{
  118. ID: id,
  119. Icon: "heater",
  120. })
  121. o.bus.RunEvent(events.HardwareState{
  122. ID: id,
  123. InternalName: mDevice.DeviceName,
  124. SupportFlags: device.SFlagPower | device.SFlagTemperature,
  125. State: o.stateMap[id],
  126. })
  127. }
  128. }
  129. // Write to device if generation is configured
  130. if o.allowsGeneration(o.genMap[id]) {
  131. targetPower := *o.stateMap[id].Power
  132. targetTemp := int(math.Round(*o.stateMap[id].Temperature))
  133. currPower := mDevice.PowerStatus > 0
  134. currTemp := mDevice.HolidayTemp
  135. changed := false
  136. if targetPower != currPower {
  137. err := o.setPower(o.genMap[id], mDevice.SubDomainID, mDevice.DeviceID, targetPower)
  138. if err != nil {
  139. return err
  140. }
  141. changed = true
  142. }
  143. if targetTemp != currTemp {
  144. err := o.setTemperature(o.genMap[id], mDevice.SubDomainID, mDevice.DeviceID, targetTemp)
  145. if err != nil {
  146. return err
  147. }
  148. changed = true
  149. }
  150. if changed {
  151. o.bus.RunEvent(events.HardwareState{
  152. ID: id,
  153. InternalName: mDevice.DeviceName,
  154. SupportFlags: device.SFlagPower | device.SFlagTemperature,
  155. State: o.stateMap[id],
  156. })
  157. }
  158. }
  159. }
  160. return nil
  161. }
  162. func (o *OnlineBridge) setPower(gen, subDomainID, internalID int, power bool) error {
  163. powerInt := 0
  164. if power {
  165. powerInt = 1
  166. }
  167. if gen == 2 {
  168. powerReq := deviceControlReqBody{
  169. SubDomain: strconv.Itoa(subDomainID),
  170. DeviceID: internalID,
  171. TestStatus: 1,
  172. Status: powerInt,
  173. }
  174. err := o.command("deviceControl", powerReq, nil)
  175. if err != nil {
  176. return err
  177. }
  178. } else if gen == 3 {
  179. powerReq := deviceControlGen3Body{
  180. Operation: "SWITCH",
  181. Status: powerInt,
  182. SubDomain: subDomainID,
  183. DeviceId: internalID,
  184. }
  185. err := o.command("deviceControlGen3ForApp", powerReq, nil)
  186. if err != nil {
  187. return err
  188. }
  189. }
  190. return nil
  191. }
  192. func (o *OnlineBridge) setTemperature(gen, subDomainID, internalID, temp int) error {
  193. if gen == 2 {
  194. tempReq := changeInfoReqBody{
  195. DeviceID: internalID,
  196. Value: temp,
  197. TimeZoneNum: "+02:00",
  198. Key: "holidayTemp",
  199. }
  200. err := o.command("changeDeviceInfo", tempReq, nil)
  201. if err != nil {
  202. return err
  203. }
  204. } else if gen == 3 {
  205. tempReq := deviceControlGen3Body{
  206. Operation: "SINGLE_CONTROL",
  207. Status: 1,
  208. SubDomain: subDomainID,
  209. DeviceId: internalID,
  210. HoldTemp: temp,
  211. }
  212. err := o.command("deviceControlGen3ForApp", tempReq, nil)
  213. if err != nil {
  214. return err
  215. }
  216. }
  217. return nil
  218. }
  219. func (o *OnlineBridge) command(command string, payload interface{}, target interface{}) error {
  220. err := o.authenticate()
  221. if err != nil {
  222. return err
  223. }
  224. url := serviceEndpoint + command
  225. method := "POST"
  226. nonce := makeNonce()
  227. timestamp := fmt.Sprintf("%d", time.Now().Unix())
  228. timeout := "300"
  229. h := sha1.New()
  230. h.Write([]byte(timeout))
  231. h.Write([]byte(timestamp))
  232. h.Write([]byte(nonce))
  233. h.Write([]byte(o.token))
  234. signature := fmt.Sprintf("%x", h.Sum(nil))
  235. body, err := json.Marshal(payload)
  236. if err != nil {
  237. return err
  238. }
  239. req, err := http.NewRequest(method, url, bytes.NewReader(body))
  240. if err != nil {
  241. return err
  242. }
  243. addDefaultHeaders(req)
  244. req.Header.Add("X-Zc-Timestamp", timestamp)
  245. req.Header.Add("X-Zc-Timeout", timeout)
  246. req.Header.Add("X-Zc-Nonce", nonce)
  247. req.Header.Add("X-Zc-User-Id", fmt.Sprintf("%d", o.userId))
  248. req.Header.Add("X-Zc-User-Signature", signature)
  249. req.Header.Add("X-Zc-Content-Length", fmt.Sprintf("%d", len(body)))
  250. res, err := http.DefaultClient.Do(req)
  251. if err != nil {
  252. return fmt.Errorf("command %s failed (%s)", command, err.Error())
  253. } else if res.StatusCode != 200 {
  254. return fmt.Errorf("command %s failed (negative response)", command)
  255. }
  256. if target == nil {
  257. return nil
  258. }
  259. err = json.NewDecoder(res.Body).Decode(&target)
  260. if err != nil {
  261. return fmt.Errorf("command %s failed (decoding response)", command)
  262. }
  263. return nil
  264. }
  265. func (o *OnlineBridge) authenticate() error {
  266. o.mx.Lock()
  267. defer o.mx.Unlock()
  268. if o.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) {
  269. body, err := json.Marshal(authReqBody{
  270. Account: o.Email,
  271. Password: o.Password,
  272. })
  273. if err != nil {
  274. return errors.New("failed to authenticate (marshaling request body)")
  275. }
  276. req, err := http.NewRequest("POST", accountEndpoint+"login", bytes.NewReader(body))
  277. if err != nil {
  278. return errors.New("failed to authenticate (creating request)")
  279. }
  280. addDefaultHeaders(req)
  281. res, err := http.DefaultClient.Do(req)
  282. if err != nil {
  283. return errors.New("failed to authenticate (internal error)")
  284. } else if res.StatusCode != 200 {
  285. return errors.New("failed to authenticate (negative result)")
  286. }
  287. var resBody authResBody
  288. err = json.NewDecoder(res.Body).Decode(&resBody)
  289. if err != nil {
  290. return errors.New("failed to authenticate (parsing response body)")
  291. }
  292. log.Printf("Mill: Authenticated as %s", resBody.NickName)
  293. o.userId = resBody.UserID
  294. o.token = resBody.Token
  295. o.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location)
  296. }
  297. return nil
  298. }
  299. func (o *OnlineBridge) allowsGeneration(gen int) bool {
  300. if gen == 2 {
  301. return o.Gen2
  302. }
  303. if gen == 3 {
  304. return o.Gen3
  305. }
  306. return false
  307. }