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.

399 lines
8.3 KiB

  1. package hue
  2. import (
  3. "context"
  4. "encoding/json"
  5. "encoding/xml"
  6. "fmt"
  7. "git.aiterp.net/lucifer/new-server/models"
  8. "log"
  9. "net/http"
  10. "strconv"
  11. "sync"
  12. "time"
  13. )
  14. type Driver struct {
  15. mu sync.Mutex
  16. bridges []*Bridge
  17. }
  18. func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
  19. if address == "" {
  20. if !dryRun {
  21. return nil, models.ErrAddressOnlyDryRunnable
  22. }
  23. res, err := http.Get("https://discovery.meethue.com")
  24. if err != nil {
  25. return nil, err
  26. }
  27. defer res.Body.Close()
  28. entries := make([]DiscoveryEntry, 0, 8)
  29. err = json.NewDecoder(res.Body).Decode(&entries)
  30. if err != nil {
  31. return nil, err
  32. }
  33. bridges := make([]models.Bridge, 0, len(entries))
  34. for _, entry := range entries {
  35. bridges = append(bridges, models.Bridge{
  36. ID: -1,
  37. Name: entry.Id,
  38. Driver: models.DTHue,
  39. Address: entry.InternalIPAddress,
  40. Token: "",
  41. })
  42. }
  43. return bridges, nil
  44. }
  45. deviceInfo := BridgeDeviceInfo{}
  46. res, err := http.Get(fmt.Sprintf("http://%s/description.xml", address))
  47. if err != nil {
  48. return nil, err
  49. }
  50. defer res.Body.Close()
  51. err = xml.NewDecoder(res.Body).Decode(&deviceInfo)
  52. if err != nil {
  53. return nil, err
  54. }
  55. bridge := models.Bridge{
  56. ID: -1,
  57. Name: deviceInfo.Device.FriendlyName,
  58. Driver: models.DTHue,
  59. Address: address,
  60. Token: "",
  61. }
  62. if !dryRun {
  63. b := &Bridge{host: address}
  64. timeout, cancel := context.WithTimeout(ctx, time.Second*30)
  65. defer cancel()
  66. ticker := time.NewTicker(time.Second)
  67. defer ticker.Stop()
  68. for range ticker.C {
  69. token, err := b.getToken(timeout)
  70. if err != nil {
  71. if err == errLinkButtonNotPressed {
  72. continue
  73. }
  74. return nil, err
  75. }
  76. bridge.Token = token
  77. b.token = token
  78. break
  79. }
  80. }
  81. return []models.Bridge{bridge}, nil
  82. }
  83. func (d *Driver) SearchDevices(ctx context.Context, bridge models.Bridge, timeout time.Duration) ([]models.Device, error) {
  84. b, err := d.ensureBridge(ctx, bridge)
  85. if err != nil {
  86. return nil, err
  87. }
  88. before, err := d.ListDevices(ctx, bridge)
  89. if err != nil {
  90. return nil, err
  91. }
  92. if timeout.Seconds() < 10 {
  93. timeout = time.Second * 10
  94. }
  95. halfTime := timeout / 2
  96. err = b.StartDiscovery(ctx, "sensors")
  97. if err != nil {
  98. return nil, err
  99. }
  100. select {
  101. case <-time.After(halfTime):
  102. case <-ctx.Done():
  103. return nil, ctx.Err()
  104. }
  105. err = b.StartDiscovery(ctx, "lights")
  106. if err != nil {
  107. return nil, err
  108. }
  109. select {
  110. case <-time.After(halfTime):
  111. case <-ctx.Done():
  112. return nil, ctx.Err()
  113. }
  114. err = b.Refresh(ctx)
  115. if err != nil {
  116. return nil, err
  117. }
  118. after, err := d.ListDevices(ctx, bridge)
  119. if err != nil {
  120. return nil, err
  121. }
  122. intersection := make([]models.Device, 0, 4)
  123. for _, device := range after {
  124. found := false
  125. for _, device2 := range before {
  126. if device2.InternalID == device.InternalID {
  127. found = true
  128. break
  129. }
  130. }
  131. if !found {
  132. intersection = append(intersection, device)
  133. }
  134. }
  135. return intersection, nil
  136. }
  137. func (d *Driver) ListDevices(ctx context.Context, bridge models.Bridge) ([]models.Device, error) {
  138. b, err := d.ensureBridge(ctx, bridge)
  139. if err != nil {
  140. return nil, err
  141. }
  142. devices := make([]models.Device, 0, 8)
  143. lightMap, err := b.getLights(ctx)
  144. if err != nil {
  145. return nil, err
  146. }
  147. for _, lightInfo := range lightMap {
  148. device := models.Device{
  149. ID: -1,
  150. BridgeID: b.externalID,
  151. InternalID: lightInfo.Uniqueid,
  152. Icon: "lightbulb",
  153. Name: lightInfo.Name,
  154. Capabilities: []models.DeviceCapability{
  155. models.DCPower,
  156. },
  157. ButtonNames: nil,
  158. DriverProperties: map[string]interface{}{
  159. "modelId": lightInfo.Modelid,
  160. "productName": lightInfo.Productname,
  161. "swVersion": lightInfo.Swversion,
  162. "hueLightType": lightInfo.Type,
  163. },
  164. UserProperties: nil,
  165. State: models.DeviceState{},
  166. Tags: nil,
  167. }
  168. hasDimming := false
  169. hasCT := false
  170. hasColor := false
  171. switch lightInfo.Type {
  172. case "On/off light":
  173. // Always take DCPower for granted anyway.
  174. case "Dimmable light":
  175. hasDimming = true
  176. case "Color temperature light":
  177. hasDimming = true
  178. hasCT = true
  179. case "Color light":
  180. hasDimming = true
  181. hasColor = true
  182. case "Extended color light":
  183. hasDimming = true
  184. hasColor = true
  185. hasCT = true
  186. }
  187. ctrl := lightInfo.Capabilities.Control
  188. if hasDimming {
  189. device.Capabilities = append(device.Capabilities, models.DCIntensity)
  190. }
  191. if hasCT {
  192. device.Capabilities = append(device.Capabilities, models.DCColorKelvin)
  193. device.DriverProperties["minKelvin"] = 1000000 / ctrl.CT.Max
  194. device.DriverProperties["maxKelvin"] = 1000000 / ctrl.CT.Min
  195. }
  196. if hasColor {
  197. device.Capabilities = append(device.Capabilities, models.DCColorHS)
  198. device.DriverProperties["gamutType"] = ctrl.Colorgamuttype
  199. device.DriverProperties["gamutData"] = ctrl.Colorgamut
  200. }
  201. device.DriverProperties["maxLumen"] = strconv.Itoa(ctrl.Maxlumen)
  202. devices = append(devices, device)
  203. }
  204. sensorMap, err := b.getSensors(ctx)
  205. if err != nil {
  206. return nil, err
  207. }
  208. for _, sensorInfo := range sensorMap {
  209. device := models.Device{
  210. ID: -1,
  211. BridgeID: b.externalID,
  212. InternalID: sensorInfo.UniqueID,
  213. Name: sensorInfo.Name,
  214. Capabilities: []models.DeviceCapability{},
  215. ButtonNames: []string{},
  216. DriverProperties: map[string]interface{}{
  217. "modelId": sensorInfo.Modelid,
  218. "productName": sensorInfo.Productname,
  219. "swVersion": sensorInfo.Swversion,
  220. "hueLightType": sensorInfo.Type,
  221. },
  222. UserProperties: nil,
  223. State: models.DeviceState{},
  224. Tags: nil,
  225. }
  226. switch sensorInfo.Type {
  227. case "ZLLSwitch":
  228. device.Capabilities = append(device.Capabilities, models.DCButtons)
  229. device.ButtonNames = append(buttonNames[:0:0], buttonNames...)
  230. device.Icon = "lightswitch"
  231. case "ZLLPresence":
  232. device.Capabilities = append(device.Capabilities, models.DCPresence)
  233. device.Icon = "sensor"
  234. case "Daylight":
  235. continue
  236. }
  237. devices = append(devices, device)
  238. }
  239. return devices, nil
  240. }
  241. func (d *Driver) Publish(ctx context.Context, bridge models.Bridge, devices []models.Device) error {
  242. b, err := d.ensureBridge(ctx, bridge)
  243. if err != nil {
  244. return err
  245. }
  246. err = b.Refresh(ctx)
  247. if err != nil {
  248. return err
  249. }
  250. b.mu.Lock()
  251. for _, device := range devices {
  252. for _, state := range b.lightStates {
  253. if device.InternalID == state.uniqueID {
  254. state.externalID = device.ID
  255. state.Update(device.State)
  256. break
  257. }
  258. }
  259. for _, state := range b.sensorStates {
  260. if device.InternalID == state.uniqueID {
  261. state.externalID = device.ID
  262. break
  263. }
  264. }
  265. }
  266. b.mu.Unlock()
  267. return b.SyncStale(ctx)
  268. }
  269. func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models.Event) error {
  270. b, err := d.ensureBridge(ctx, bridge)
  271. if err != nil {
  272. return err
  273. }
  274. fastTicker := time.NewTicker(time.Second / 10)
  275. slowTicker := time.NewTicker(time.Second / 3)
  276. selectedTicker := fastTicker
  277. ticksUntilRefresh := 0
  278. ticksSinceChange := 0
  279. for {
  280. select {
  281. case <-selectedTicker.C:
  282. if ticksUntilRefresh <= 0 {
  283. err := b.Refresh(ctx)
  284. if err != nil {
  285. return err
  286. }
  287. err = b.SyncStale(ctx)
  288. if err != nil {
  289. return err
  290. }
  291. ticksUntilRefresh = 60
  292. }
  293. events, err := b.SyncSensors(ctx)
  294. if err != nil {
  295. return err
  296. }
  297. for _, event := range events {
  298. ch <- event
  299. ticksSinceChange = 0
  300. }
  301. if ticksSinceChange > 30 {
  302. selectedTicker = slowTicker
  303. } else if ticksSinceChange == 0 {
  304. selectedTicker = fastTicker
  305. }
  306. case <-ctx.Done():
  307. return ctx.Err()
  308. }
  309. ticksUntilRefresh -= 1
  310. ticksSinceChange += 1
  311. }
  312. }
  313. func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) {
  314. d.mu.Lock()
  315. for _, bridge := range d.bridges {
  316. if bridge.host == info.Address {
  317. d.mu.Unlock()
  318. return bridge, nil
  319. }
  320. }
  321. d.mu.Unlock()
  322. bridge := &Bridge{
  323. host: info.Address,
  324. token: info.Token,
  325. externalID: info.ID,
  326. }
  327. // If this call succeeds, then the token is ok.
  328. lightMap, err := bridge.getLights(ctx)
  329. if err != nil {
  330. return nil, err
  331. }
  332. log.Printf("Found %d lights on bridge %d", len(lightMap), bridge.externalID)
  333. // To avoid a potential duplicate, try looking for it again before inserting
  334. d.mu.Lock()
  335. for _, bridge := range d.bridges {
  336. if bridge.host == info.Address {
  337. d.mu.Unlock()
  338. return bridge, nil
  339. }
  340. }
  341. d.bridges = append(d.bridges, bridge)
  342. d.mu.Unlock()
  343. return bridge, nil
  344. }