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.

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