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.

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