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.

426 lines
8.6 KiB

  1. package hue
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "git.aiterp.net/lucifer/new-server/internal/lerrors"
  8. "git.aiterp.net/lucifer/new-server/models"
  9. "golang.org/x/sync/errgroup"
  10. "io"
  11. "net"
  12. "net/http"
  13. "strconv"
  14. "strings"
  15. "sync"
  16. "time"
  17. )
  18. type Bridge struct {
  19. mu sync.Mutex
  20. host string
  21. token string
  22. externalID int
  23. lightStates []*hueLightState
  24. sensorStates []*hueSensorState
  25. quarantine map[string]time.Time
  26. syncingPublish uint32
  27. }
  28. func (b *Bridge) Refresh(ctx context.Context) error {
  29. lightMap, err := b.getLights(ctx)
  30. if err != nil {
  31. return err
  32. }
  33. b.mu.Lock()
  34. for index, light := range lightMap {
  35. if time.Now().Before(b.quarantine[light.Uniqueid]) {
  36. continue
  37. }
  38. var state *hueLightState
  39. for _, existingState := range b.lightStates {
  40. if existingState.index == index {
  41. state = existingState
  42. }
  43. }
  44. if state == nil {
  45. state = &hueLightState{
  46. index: index,
  47. uniqueID: light.Uniqueid,
  48. externalID: -1,
  49. info: light,
  50. }
  51. b.lightStates = append(b.lightStates, state)
  52. } else {
  53. if light.Uniqueid != state.uniqueID {
  54. state.uniqueID = light.Uniqueid
  55. state.externalID = -1
  56. }
  57. }
  58. state.CheckStaleness(light.State)
  59. }
  60. b.mu.Unlock()
  61. sensorMap, err := b.getSensors(ctx)
  62. if err != nil {
  63. return err
  64. }
  65. b.mu.Lock()
  66. for index, sensor := range sensorMap {
  67. if time.Now().Before(b.quarantine[sensor.UniqueID]) {
  68. continue
  69. }
  70. var state *hueSensorState
  71. for _, existingState := range b.sensorStates {
  72. if existingState.index == index {
  73. state = existingState
  74. }
  75. }
  76. if state == nil {
  77. state = &hueSensorState{
  78. index: index,
  79. uniqueID: sensor.UniqueID,
  80. externalID: -1,
  81. presenceCooldown: -2,
  82. }
  83. b.sensorStates = append(b.sensorStates, state)
  84. } else {
  85. if sensor.UniqueID != state.uniqueID {
  86. state.uniqueID = sensor.UniqueID
  87. state.externalID = -1
  88. }
  89. }
  90. }
  91. b.mu.Unlock()
  92. return nil
  93. }
  94. func (b *Bridge) SyncStale(ctx context.Context) error {
  95. indices := make([]int, 0, 4)
  96. inputs := make([]LightStateInput, 0, 4)
  97. eg, ctx := errgroup.WithContext(ctx)
  98. b.mu.Lock()
  99. for _, state := range b.lightStates {
  100. if !state.stale {
  101. continue
  102. }
  103. indices = append(indices, state.index)
  104. inputs = append(inputs, state.input)
  105. }
  106. b.mu.Unlock()
  107. if len(inputs) == 0 {
  108. return nil
  109. }
  110. for i, input := range inputs {
  111. iCopy := i
  112. index := indices[i]
  113. inputCopy := input
  114. eg.Go(func() error {
  115. err := b.putLightState(ctx, index, inputCopy)
  116. if err != nil {
  117. return err
  118. }
  119. b.lightStates[iCopy].stale = false
  120. return nil
  121. })
  122. }
  123. return eg.Wait()
  124. }
  125. func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error {
  126. // Find index
  127. b.mu.Lock()
  128. found := false
  129. index := -1
  130. for i, ls := range b.lightStates {
  131. if ls.uniqueID == device.InternalID {
  132. found = true
  133. index = i
  134. }
  135. }
  136. b.mu.Unlock()
  137. if !found {
  138. return lerrors.ErrNotFound
  139. }
  140. // Delete light from bridge
  141. err := b.deleteLight(ctx, index)
  142. if err != nil {
  143. return err
  144. }
  145. // Remove light state from local list. I don't know if the quarantine is necessary, but let's have it anyway.
  146. b.mu.Lock()
  147. for i, ls := range b.lightStates {
  148. if ls.uniqueID == device.InternalID {
  149. b.lightStates = append(b.lightStates[:i], b.lightStates[i+1:]...)
  150. }
  151. }
  152. if b.quarantine == nil {
  153. b.quarantine = make(map[string]time.Time, 1)
  154. }
  155. b.quarantine[device.InternalID] = time.Now().Add(time.Second * 30)
  156. b.mu.Unlock()
  157. return nil
  158. }
  159. func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) {
  160. sensorMap, err := b.getSensors(ctx)
  161. if err != nil {
  162. return nil, err
  163. }
  164. var events []models.Event
  165. b.mu.Lock()
  166. for idx, sensorData := range sensorMap {
  167. for _, state := range b.sensorStates {
  168. if idx == state.index {
  169. event := state.Update(sensorData)
  170. if event != nil {
  171. events = append(events, *event)
  172. }
  173. break
  174. }
  175. }
  176. }
  177. b.mu.Unlock()
  178. return events, nil
  179. }
  180. func (b *Bridge) StartDiscovery(ctx context.Context, model string) error {
  181. return b.post(ctx, model, nil, nil)
  182. }
  183. func (b *Bridge) putLightState(ctx context.Context, index int, input LightStateInput) error {
  184. return b.put(ctx, fmt.Sprintf("lights/%d/state", index), input, nil)
  185. }
  186. func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightStateInput) error {
  187. return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, nil)
  188. }
  189. func (b *Bridge) deleteLight(ctx context.Context, index int) error {
  190. return b.delete(ctx, fmt.Sprintf("lights/%d", index), nil)
  191. }
  192. func (b *Bridge) getToken(ctx context.Context) (string, error) {
  193. result := make([]CreateUserResponse, 0, 1)
  194. err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
  195. if err != nil {
  196. return "", err
  197. }
  198. if len(result) == 0 || result[0].Error != nil {
  199. return "", errLinkButtonNotPressed
  200. }
  201. if result[0].Success == nil {
  202. return "", lerrors.ErrUnexpectedResponse
  203. }
  204. return result[0].Success.Username, nil
  205. }
  206. func (b *Bridge) getLights(ctx context.Context) (map[int]LightData, error) {
  207. result := make(map[int]LightData, 16)
  208. err := b.get(ctx, "lights", &result)
  209. if err != nil {
  210. return nil, err
  211. }
  212. return result, nil
  213. }
  214. func (b *Bridge) getSensors(ctx context.Context) (map[int]SensorData, error) {
  215. result := make(map[int]SensorData, 16)
  216. err := b.get(ctx, "sensors", &result)
  217. if err != nil {
  218. return nil, err
  219. }
  220. return result, nil
  221. }
  222. func (b *Bridge) getGroups(ctx context.Context) (map[int]GroupData, error) {
  223. result := make(map[int]GroupData, 16)
  224. err := b.get(ctx, "groups", &result)
  225. if err != nil {
  226. return nil, err
  227. }
  228. return result, nil
  229. }
  230. func (b *Bridge) postGroup(ctx context.Context, input GroupData) (int, error) {
  231. var res []struct {
  232. Success struct {
  233. ID string `json:"id"`
  234. } `json:"success"`
  235. }
  236. err := b.post(ctx, "groups", input, &res)
  237. id, _ := strconv.Atoi(res[0].Success.ID)
  238. return id, err
  239. }
  240. func (b *Bridge) deleteGroup(ctx context.Context, index int) error {
  241. return b.delete(ctx, "groups/"+strconv.Itoa(index), nil)
  242. }
  243. func (b *Bridge) get(ctx context.Context, resource string, target interface{}) error {
  244. if b.token != "" {
  245. resource = b.token + "/" + resource
  246. }
  247. req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
  248. if err != nil {
  249. return err
  250. }
  251. res, err := httpClient.Do(req.WithContext(ctx))
  252. if err != nil {
  253. return err
  254. }
  255. defer res.Body.Close()
  256. return json.NewDecoder(res.Body).Decode(target)
  257. }
  258. func (b *Bridge) delete(ctx context.Context, resource string, target interface{}) error {
  259. if b.token != "" {
  260. resource = b.token + "/" + resource
  261. }
  262. req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/%s", b.host, resource), nil)
  263. if err != nil {
  264. return err
  265. }
  266. res, err := httpClient.Do(req.WithContext(ctx))
  267. if err != nil {
  268. return err
  269. }
  270. defer res.Body.Close()
  271. if target == nil {
  272. return nil
  273. }
  274. return json.NewDecoder(res.Body).Decode(target)
  275. }
  276. func (b *Bridge) post(ctx context.Context, resource string, body interface{}, target interface{}) error {
  277. rb, err := reqBody(body)
  278. if err != nil {
  279. return err
  280. }
  281. if b.token != "" {
  282. resource = b.token + "/" + resource
  283. }
  284. req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
  285. if err != nil {
  286. return err
  287. }
  288. res, err := httpClient.Do(req.WithContext(ctx))
  289. if err != nil {
  290. return err
  291. }
  292. defer res.Body.Close()
  293. if target == nil {
  294. return nil
  295. }
  296. return json.NewDecoder(res.Body).Decode(target)
  297. }
  298. func (b *Bridge) put(ctx context.Context, resource string, body interface{}, target interface{}) error {
  299. rb, err := reqBody(body)
  300. if err != nil {
  301. return err
  302. }
  303. if b.token != "" {
  304. resource = b.token + "/" + resource
  305. }
  306. req, err := http.NewRequest("PUT", fmt.Sprintf("http://%s/api/%s", b.host, resource), rb)
  307. if err != nil {
  308. return err
  309. }
  310. res, err := httpClient.Do(req.WithContext(ctx))
  311. if err != nil {
  312. return err
  313. }
  314. defer res.Body.Close()
  315. if target == nil {
  316. return nil
  317. }
  318. return json.NewDecoder(res.Body).Decode(target)
  319. }
  320. func reqBody(body interface{}) (io.Reader, error) {
  321. if body == nil {
  322. return nil, nil
  323. }
  324. switch v := body.(type) {
  325. case []byte:
  326. return bytes.NewReader(v), nil
  327. case string:
  328. return strings.NewReader(v), nil
  329. case io.Reader:
  330. return v, nil
  331. default:
  332. jsonData, err := json.Marshal(v)
  333. if err != nil {
  334. return nil, err
  335. }
  336. return bytes.NewReader(jsonData), nil
  337. }
  338. }
  339. var httpClient = &http.Client{
  340. Transport: &http.Transport{
  341. Proxy: http.ProxyFromEnvironment,
  342. DialContext: (&net.Dialer{
  343. Timeout: 30 * time.Second,
  344. KeepAlive: 30 * time.Second,
  345. }).DialContext,
  346. MaxIdleConns: 256,
  347. MaxIdleConnsPerHost: 16,
  348. IdleConnTimeout: 10 * time.Minute,
  349. },
  350. Timeout: time.Minute,
  351. }