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.

402 lines
8.4 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 "ZLLTemperature":
  235. device.Capabilities = append(device.Capabilities, models.DCTemperatureSensor)
  236. device.Icon = "thermometer"
  237. case "Daylight":
  238. continue
  239. }
  240. devices = append(devices, device)
  241. }
  242. return devices, nil
  243. }
  244. func (d *Driver) Publish(ctx context.Context, bridge models.Bridge, devices []models.Device) error {
  245. b, err := d.ensureBridge(ctx, bridge)
  246. if err != nil {
  247. return err
  248. }
  249. err = b.Refresh(ctx)
  250. if err != nil {
  251. return err
  252. }
  253. b.mu.Lock()
  254. for _, device := range devices {
  255. for _, state := range b.lightStates {
  256. if device.InternalID == state.uniqueID {
  257. state.externalID = device.ID
  258. state.Update(device.State)
  259. break
  260. }
  261. }
  262. for _, state := range b.sensorStates {
  263. if device.InternalID == state.uniqueID {
  264. state.externalID = device.ID
  265. break
  266. }
  267. }
  268. }
  269. b.mu.Unlock()
  270. return b.SyncStale(ctx)
  271. }
  272. func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models.Event) error {
  273. b, err := d.ensureBridge(ctx, bridge)
  274. if err != nil {
  275. return err
  276. }
  277. fastTicker := time.NewTicker(time.Second / 10)
  278. slowTicker := time.NewTicker(time.Second / 3)
  279. selectedTicker := fastTicker
  280. ticksUntilRefresh := 0
  281. ticksSinceChange := 0
  282. for {
  283. select {
  284. case <-selectedTicker.C:
  285. if ticksUntilRefresh <= 0 {
  286. err := b.Refresh(ctx)
  287. if err != nil {
  288. return err
  289. }
  290. err = b.SyncStale(ctx)
  291. if err != nil {
  292. return err
  293. }
  294. ticksUntilRefresh = 60
  295. }
  296. events, err := b.SyncSensors(ctx)
  297. if err != nil {
  298. return err
  299. }
  300. for _, event := range events {
  301. ch <- event
  302. ticksSinceChange = 0
  303. }
  304. if ticksSinceChange > 30 {
  305. selectedTicker = slowTicker
  306. } else if ticksSinceChange == 0 {
  307. selectedTicker = fastTicker
  308. }
  309. case <-ctx.Done():
  310. return ctx.Err()
  311. }
  312. ticksUntilRefresh -= 1
  313. ticksSinceChange += 1
  314. }
  315. }
  316. func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) {
  317. d.mu.Lock()
  318. for _, bridge := range d.bridges {
  319. if bridge.host == info.Address {
  320. d.mu.Unlock()
  321. return bridge, nil
  322. }
  323. }
  324. d.mu.Unlock()
  325. bridge := &Bridge{
  326. host: info.Address,
  327. token: info.Token,
  328. externalID: info.ID,
  329. }
  330. // If this call succeeds, then the token is ok.
  331. lightMap, err := bridge.getLights(ctx)
  332. if err != nil {
  333. return nil, err
  334. }
  335. log.Printf("Found %d lights on bridge %d", len(lightMap), bridge.externalID)
  336. // To avoid a potential duplicate, try looking for it again before inserting
  337. d.mu.Lock()
  338. for _, bridge := range d.bridges {
  339. if bridge.host == info.Address {
  340. d.mu.Unlock()
  341. return bridge, nil
  342. }
  343. }
  344. d.bridges = append(d.bridges, bridge)
  345. d.mu.Unlock()
  346. return bridge, nil
  347. }