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.

437 lines
8.8 KiB

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