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.

494 lines
14 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. package hue
  2. import (
  3. "encoding/json"
  4. "encoding/xml"
  5. "fmt"
  6. "git.aiterp.net/lucifer3/server/device"
  7. "git.aiterp.net/lucifer3/server/events"
  8. "git.aiterp.net/lucifer3/server/internal/color"
  9. "git.aiterp.net/lucifer3/server/internal/gentools"
  10. "strings"
  11. "time"
  12. )
  13. type DeviceData struct {
  14. ID string `json:"id"`
  15. LegacyID string `json:"id_v1"`
  16. Metadata DeviceMetadata `json:"metadata"`
  17. Type string `json:"type"`
  18. ProductData DeviceProductData `json:"product_data"`
  19. Services []ResourceLink `json:"services"`
  20. }
  21. type DeviceMetadata struct {
  22. Archetype string `json:"archetype"`
  23. Name string `json:"name"`
  24. }
  25. type DeviceProductData struct {
  26. Certified bool `json:"certified"`
  27. ManufacturerName string `json:"manufacturer_name"`
  28. ModelID string `json:"model_id"`
  29. ProductArchetype string `json:"product_archetype"`
  30. ProductName string `json:"product_name"`
  31. SoftwareVersion string `json:"software_version"`
  32. }
  33. type SSEUpdate struct {
  34. CreationTime time.Time `json:"creationTime"`
  35. ID string `json:"id"`
  36. Type string `json:"type"`
  37. Data []ResourceData `json:"data"`
  38. }
  39. type ResourceData struct {
  40. ID string `json:"id"`
  41. LegacyID string `json:"id_v1"`
  42. Metadata DeviceMetadata `json:"metadata"`
  43. Type string `json:"type"`
  44. Mode *string `json:"mode"`
  45. Owner *ResourceLink `json:"owner"`
  46. ProductData *DeviceProductData `json:"product_data"`
  47. Services []ResourceLink `json:"services"`
  48. Button *SensorButton `json:"button"`
  49. Power *LightPower `json:"on"`
  50. Color *LightColor `json:"color"`
  51. ColorTemperature *LightCT `json:"color_temperature"`
  52. Dimming *LightDimming `json:"dimming"`
  53. Dynamics *LightDynamics `json:"dynamics"`
  54. Alert *LightAlert `json:"alert"`
  55. PowerState *PowerState `json:"power_state"`
  56. Temperature *SensorTemperature `json:"temperature"`
  57. Motion *SensorMotion `json:"motion"`
  58. Status *string `json:"status"`
  59. }
  60. func (res *ResourceData) ServiceID(kind string) *string {
  61. for _, ptr := range res.Services {
  62. if ptr.Kind == kind {
  63. return &ptr.ID
  64. }
  65. }
  66. return nil
  67. }
  68. func (res *ResourceData) ServiceIndex(kind string, id string) int {
  69. idx := 0
  70. for _, link := range res.Services {
  71. if link.ID == id {
  72. return idx
  73. } else if link.Kind == kind {
  74. idx += 1
  75. }
  76. }
  77. return -1
  78. }
  79. func (res *ResourceData) FixState(state device.State, resources map[string]*ResourceData) device.State {
  80. fixedState := device.State{}
  81. if lightID := res.ServiceID("light"); lightID != nil {
  82. if light := resources[*lightID]; light != nil {
  83. if state.Color != nil {
  84. if state.Color.IsKelvin() {
  85. if light.ColorTemperature != nil {
  86. mirek := 1000000 / *state.Color.K
  87. if mirek < light.ColorTemperature.MirekSchema.MirekMinimum {
  88. mirek = light.ColorTemperature.MirekSchema.MirekMinimum
  89. }
  90. if mirek > light.ColorTemperature.MirekSchema.MirekMaximum {
  91. mirek = light.ColorTemperature.MirekSchema.MirekMaximum
  92. }
  93. fixedState.Color = &color.Color{K: gentools.Ptr(1000000 / mirek)}
  94. }
  95. } else {
  96. if light.Color != nil {
  97. if col, ok := state.Color.ToXY(); ok {
  98. col.XY = gentools.Ptr(light.Color.Gamut.Conform(*col.XY))
  99. fixedState.Color = &col
  100. }
  101. }
  102. }
  103. }
  104. if state.Intensity != nil && light.Dimming != nil {
  105. fixedState.Intensity = gentools.ShallowCopy(state.Intensity)
  106. }
  107. if state.Power != nil && light.Power != nil {
  108. fixedState.Power = gentools.ShallowCopy(state.Power)
  109. }
  110. }
  111. }
  112. return fixedState
  113. }
  114. func (res *ResourceData) WithUpdate(update ResourceUpdate) *ResourceData {
  115. resCopy := *res
  116. if update.Name != nil {
  117. resCopy.Metadata.Name = *update.Name
  118. }
  119. if update.Power != nil {
  120. cp := *resCopy.Power
  121. resCopy.Power = &cp
  122. resCopy.Power.On = *update.Power
  123. }
  124. if update.ColorXY != nil {
  125. cp := *resCopy.Color
  126. resCopy.Color = &cp
  127. resCopy.Color.XY = *update.ColorXY
  128. if resCopy.ColorTemperature != nil {
  129. cp := *resCopy.ColorTemperature
  130. resCopy.ColorTemperature = &cp
  131. resCopy.ColorTemperature.Mirek = nil
  132. }
  133. }
  134. if update.Mirek != nil {
  135. cp := *resCopy.ColorTemperature
  136. resCopy.ColorTemperature = &cp
  137. mirek := *update.Mirek
  138. resCopy.ColorTemperature.Mirek = &mirek
  139. }
  140. if update.Brightness != nil {
  141. cp := *resCopy.Dimming
  142. resCopy.Dimming = &cp
  143. resCopy.Dimming.Brightness = *update.Brightness
  144. }
  145. return &resCopy
  146. }
  147. func (res *ResourceData) WithPatch(patch ResourceData) *ResourceData {
  148. res2 := *res
  149. if patch.Color != nil {
  150. res2.Color = gentools.ShallowCopy(res2.Color)
  151. res2.Color.XY = patch.Color.XY
  152. }
  153. if patch.ColorTemperature != nil {
  154. res2.ColorTemperature = gentools.ShallowCopy(res2.ColorTemperature)
  155. res2.ColorTemperature.Mirek = gentools.ShallowCopy(patch.ColorTemperature.Mirek)
  156. }
  157. gentools.ShallowCopyTo(&res2.ProductData, patch.ProductData)
  158. gentools.ShallowCopyTo(&res2.Button, patch.Button)
  159. gentools.ShallowCopyTo(&res2.Power, patch.Power)
  160. gentools.ShallowCopyTo(&res2.Dimming, patch.Dimming)
  161. gentools.ShallowCopyTo(&res2.Dynamics, patch.Dynamics)
  162. gentools.ShallowCopyTo(&res2.Alert, patch.Alert)
  163. gentools.ShallowCopyTo(&res2.PowerState, patch.PowerState)
  164. gentools.ShallowCopyTo(&res2.Temperature, patch.Temperature)
  165. gentools.ShallowCopyTo(&res2.Motion, patch.Motion)
  166. gentools.ShallowCopyTo(&res2.Status, patch.Status)
  167. return &res2
  168. }
  169. func (res *ResourceData) GenerateEvent(hostname string, resources map[string]*ResourceData) (events.HardwareState, events.HardwareMetadata) {
  170. hwState := events.HardwareState{
  171. ID: fmt.Sprintf("hue:%s:%s", hostname, res.ID),
  172. InternalName: res.Metadata.Name,
  173. State: res.GenerateState(resources),
  174. Unreachable: false,
  175. }
  176. hwMeta := events.HardwareMetadata{
  177. ID: hwState.ID,
  178. }
  179. buttonCount := 0
  180. switch res.ProductData.ProductArchetype {
  181. case "candle_bulb":
  182. hwMeta.Icon = "hue_lightbulb_e14"
  183. case "sultan_bulb":
  184. hwMeta.Icon = "hue_lightbulb_e27"
  185. case "hue_signe":
  186. hwMeta.Icon = "hue_signe"
  187. case "unknown_archetype":
  188. switch res.ProductData.ProductName {
  189. case "Hue motion sensor":
  190. hwMeta.Icon = "hue_motionsensor"
  191. case "Hue dimmer switch":
  192. hwMeta.Icon = "hue_dimmerswitch"
  193. }
  194. }
  195. if res.ProductData != nil {
  196. hwMeta.FirmwareVersion = res.ProductData.SoftwareVersion
  197. }
  198. for _, ptr := range res.Services {
  199. svc := resources[ptr.ID]
  200. if svc == nil {
  201. continue // I've never seen this happen, but safety first.
  202. }
  203. switch ptr.Kind {
  204. case "zigbee_connectivity":
  205. switch *svc.Status {
  206. case "connectivity_issue":
  207. hwState.Unreachable = true
  208. }
  209. case "device_power":
  210. hwState.BatteryPercentage = gentools.Ptr(int(svc.PowerState.BatteryLevel))
  211. case "button":
  212. buttonCount += 1
  213. hwState.SupportFlags |= device.SFlagSensorButtons
  214. case "motion":
  215. hwState.SupportFlags |= device.SFlagSensorPresence
  216. case "temperature":
  217. hwState.SupportFlags |= device.SFlagSensorTemperature
  218. case "light":
  219. if svc.Power != nil {
  220. hwState.SupportFlags |= device.SFlagPower
  221. }
  222. if svc.Dimming != nil {
  223. hwState.SupportFlags |= device.SFlagIntensity
  224. }
  225. if svc.ColorTemperature != nil {
  226. hwState.SupportFlags |= device.SFlagColor
  227. hwState.ColorFlags |= device.CFlagKelvin
  228. if svc.ColorTemperature.MirekSchema.MirekMinimum != 0 {
  229. hwState.ColorKelvinRange = &[2]int{
  230. 1000000 / svc.ColorTemperature.MirekSchema.MirekMinimum,
  231. 1000000 / svc.ColorTemperature.MirekSchema.MirekMaximum,
  232. }
  233. }
  234. }
  235. if svc.Color != nil {
  236. hwState.SupportFlags |= device.SFlagColor
  237. hwState.ColorFlags |= device.CFlagXY
  238. hwState.ColorGamut = gentools.Ptr(svc.Color.Gamut)
  239. hwState.ColorGamut.Label = svc.Color.GamutType
  240. }
  241. }
  242. }
  243. if buttonCount == 4 {
  244. hwState.Buttons = []string{"On", "DimUp", "DimDown", "Off"}
  245. } else if buttonCount == 1 {
  246. hwState.Buttons = []string{"Button"}
  247. } else {
  248. for n := 1; n <= buttonCount; n++ {
  249. hwState.Buttons = append(hwState.Buttons, fmt.Sprint("Button", n))
  250. }
  251. }
  252. return hwState, hwMeta
  253. }
  254. func (res *ResourceData) GenerateState(resources map[string]*ResourceData) device.State {
  255. state := device.State{}
  256. for _, ptr := range res.Services {
  257. switch ptr.Kind {
  258. case "light":
  259. light := resources[ptr.ID]
  260. if light == nil {
  261. continue
  262. }
  263. if light.Power != nil {
  264. state.Power = gentools.Ptr(light.Power.On)
  265. }
  266. if light.Dimming != nil {
  267. state.Intensity = gentools.Ptr(light.Dimming.Brightness / 100.0)
  268. }
  269. if light.ColorTemperature != nil {
  270. if light.ColorTemperature.Mirek != nil {
  271. state.Color = &color.Color{K: gentools.Ptr(1000000 / *light.ColorTemperature.Mirek)}
  272. }
  273. }
  274. if light.Color != nil {
  275. if state.Color == nil || state.Color.IsEmpty() {
  276. state.Color = &color.Color{XY: &light.Color.XY}
  277. }
  278. }
  279. }
  280. }
  281. return state
  282. }
  283. type SensorButton struct {
  284. LastEvent string `json:"last_event"`
  285. }
  286. type SensorMotion struct {
  287. Motion bool `json:"motion"`
  288. Valid bool `json:"motion_valid"`
  289. }
  290. type SensorTemperature struct {
  291. Temperature float64 `json:"temperature"`
  292. Valid bool `json:"temperature_valid"`
  293. }
  294. type PowerState struct {
  295. BatteryState string `json:"battery_state"`
  296. BatteryLevel float64 `json:"battery_level"`
  297. }
  298. type LightPower struct {
  299. On bool `json:"on"`
  300. }
  301. type LightDimming struct {
  302. Brightness float64 `json:"brightness"`
  303. }
  304. type LightColor struct {
  305. Gamut color.Gamut `json:"gamut"`
  306. GamutType string `json:"gamut_type"`
  307. XY color.XY `json:"xy"`
  308. }
  309. type LightCT struct {
  310. Mirek *int `json:"mirek"`
  311. MirekSchema LightCTMirekSchema `json:"mirek_schema"`
  312. MirekValid bool `json:"mirek_valid"`
  313. }
  314. type LightCTMirekSchema struct {
  315. MirekMaximum int `json:"mirek_maximum"`
  316. MirekMinimum int `json:"mirek_minimum"`
  317. }
  318. type LightDynamics struct {
  319. Speed float64 `json:"speed"`
  320. SpeedValid bool `json:"speed_valid"`
  321. Status string `json:"status"`
  322. StatusValues []string `json:"status_values"`
  323. }
  324. type LightAlert struct {
  325. ActionValues []string `json:"action_values"`
  326. }
  327. type ResourceUpdate struct {
  328. Name *string
  329. Power *bool
  330. ColorXY *color.XY
  331. Brightness *float64
  332. Mirek *int
  333. TransitionDuration *time.Duration
  334. }
  335. func (r ResourceUpdate) Split() []ResourceUpdate {
  336. updates := make([]ResourceUpdate, 0, 2)
  337. if r.Name != nil {
  338. updates = append(updates, ResourceUpdate{Name: r.Name, TransitionDuration: r.TransitionDuration})
  339. }
  340. if r.Power != nil {
  341. updates = append(updates, ResourceUpdate{Power: r.Power, TransitionDuration: r.TransitionDuration})
  342. }
  343. if r.ColorXY != nil {
  344. updates = append(updates, ResourceUpdate{ColorXY: r.ColorXY, TransitionDuration: r.TransitionDuration})
  345. }
  346. if r.Brightness != nil {
  347. updates = append(updates, ResourceUpdate{Brightness: r.Brightness, TransitionDuration: r.TransitionDuration})
  348. }
  349. if r.Mirek != nil {
  350. updates = append(updates, ResourceUpdate{Mirek: r.Mirek, TransitionDuration: r.TransitionDuration})
  351. }
  352. return updates
  353. }
  354. func (r ResourceUpdate) MarshalJSON() ([]byte, error) {
  355. chunks := make([]string, 0, 4)
  356. if r.Name != nil {
  357. s, _ := json.Marshal(*r.Name)
  358. chunks = append(chunks, fmt.Sprintf(`"metadata":{"name":%s}`, string(s)))
  359. }
  360. if r.Power != nil {
  361. chunks = append(chunks, fmt.Sprintf(`"on":{"on":%v}`, *r.Power))
  362. }
  363. if r.ColorXY != nil {
  364. chunks = append(chunks, fmt.Sprintf(`"color":{"xy":{"x":%f,"y":%f}}`, r.ColorXY.X, r.ColorXY.Y))
  365. }
  366. if r.Brightness != nil {
  367. chunks = append(chunks, fmt.Sprintf(`"dimming":{"brightness":%f}`, *r.Brightness))
  368. }
  369. if r.Mirek != nil {
  370. chunks = append(chunks, fmt.Sprintf(`"color_temperature":{"mirek":%d}`, *r.Mirek))
  371. }
  372. if r.TransitionDuration != nil {
  373. chunks = append(chunks, fmt.Sprintf(`"dynamics":{"duration":%d}`, r.TransitionDuration.Truncate(time.Millisecond*100).Milliseconds()))
  374. }
  375. return []byte(fmt.Sprintf("{%s}", strings.Join(chunks, ","))), nil
  376. }
  377. type ResourceLink struct {
  378. ID string `json:"rid"`
  379. Kind string `json:"rtype"`
  380. }
  381. func (rl *ResourceLink) Path() string {
  382. return fmt.Sprintf("/clip/v2/resource/%s/%s", rl.Kind, rl.ID)
  383. }
  384. type CreateUserInput struct {
  385. DeviceType string `json:"devicetype"`
  386. }
  387. type CreateUserResponse struct {
  388. Success *struct {
  389. Username string `json:"username"`
  390. } `json:"success"`
  391. Error *struct {
  392. Type int `json:"type"`
  393. Address string `json:"address"`
  394. Description string `json:"description"`
  395. } `json:"error"`
  396. }
  397. type DiscoveryEntry struct {
  398. Id string `json:"id"`
  399. InternalIPAddress string `json:"internalipaddress"`
  400. }
  401. type BridgeDeviceInfo struct {
  402. XMLName xml.Name `xml:"root"`
  403. Text string `xml:",chardata"`
  404. Xmlns string `xml:"xmlns,attr"`
  405. SpecVersion struct {
  406. Text string `xml:",chardata"`
  407. Major string `xml:"major"`
  408. Minor string `xml:"minor"`
  409. } `xml:"specVersion"`
  410. URLBase string `xml:"URLBase"`
  411. Device struct {
  412. Text string `xml:",chardata"`
  413. DeviceType string `xml:"deviceType"`
  414. FriendlyName string `xml:"friendlyName"`
  415. Manufacturer string `xml:"manufacturer"`
  416. ManufacturerURL string `xml:"manufacturerURL"`
  417. ModelDescription string `xml:"modelDescription"`
  418. ModelName string `xml:"modelName"`
  419. ModelNumber string `xml:"modelNumber"`
  420. ModelURL string `xml:"modelURL"`
  421. SerialNumber string `xml:"serialNumber"`
  422. UDN string `xml:"UDN"`
  423. PresentationURL string `xml:"presentationURL"`
  424. IconList struct {
  425. Text string `xml:",chardata"`
  426. Icon struct {
  427. Text string `xml:",chardata"`
  428. Mimetype string `xml:"mimetype"`
  429. Height string `xml:"height"`
  430. Width string `xml:"width"`
  431. Depth string `xml:"depth"`
  432. URL string `xml:"url"`
  433. } `xml:"icon"`
  434. } `xml:"iconList"`
  435. } `xml:"device"`
  436. }