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.

420 lines
8.9 KiB

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