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.

452 lines
12 KiB

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.Color, patch.Color)
  161. gentools.ShallowCopyTo(&res2.ColorTemperature, patch.ColorTemperature)
  162. gentools.ShallowCopyTo(&res2.Dimming, patch.Dimming)
  163. gentools.ShallowCopyTo(&res2.Dynamics, patch.Dynamics)
  164. gentools.ShallowCopyTo(&res2.Alert, patch.Alert)
  165. gentools.ShallowCopyTo(&res2.PowerState, patch.PowerState)
  166. gentools.ShallowCopyTo(&res2.Temperature, patch.Temperature)
  167. gentools.ShallowCopyTo(&res2.Motion, patch.Motion)
  168. gentools.ShallowCopyTo(&res2.Status, patch.Status)
  169. return &res2
  170. }
  171. func (res *ResourceData) GenerateEvent(hostname string, resources map[string]*ResourceData) (events.HardwareState, events.HardwareMetadata) {
  172. hwState := events.HardwareState{
  173. ID: fmt.Sprintf("hue:%s:%s", hostname, res.ID),
  174. InternalName: res.Metadata.Name,
  175. State: res.GenerateState(resources),
  176. Unreachable: false,
  177. }
  178. hwMeta := events.HardwareMetadata{
  179. ID: hwState.ID,
  180. }
  181. buttonCount := 0
  182. if res.ProductData != nil {
  183. hwMeta.FirmwareVersion = res.ProductData.SoftwareVersion
  184. }
  185. for _, ptr := range res.Services {
  186. svc := resources[ptr.ID]
  187. if svc == nil {
  188. continue // I've never seen this happen, but safety first.
  189. }
  190. switch ptr.Kind {
  191. case "device_power":
  192. hwState.BatteryPercentage = gentools.Ptr(int(svc.PowerState.BatteryLevel))
  193. case "button":
  194. buttonCount += 1
  195. hwState.SupportFlags |= device.SFlagSensorButtons
  196. case "motion":
  197. hwState.SupportFlags |= device.SFlagSensorPresence
  198. case "temperature":
  199. hwState.SupportFlags |= device.SFlagSensorTemperature
  200. case "light":
  201. if svc.Power != nil {
  202. hwState.SupportFlags |= device.SFlagPower
  203. }
  204. if svc.Dimming != nil {
  205. hwState.SupportFlags |= device.SFlagIntensity
  206. }
  207. if svc.ColorTemperature != nil {
  208. hwState.SupportFlags |= device.SFlagColor
  209. hwState.ColorFlags |= device.CFlagKelvin
  210. hwState.TemperatureRange = &[2]int{
  211. 1000000 / svc.ColorTemperature.MirekSchema.MirekMinimum,
  212. 1000000 / svc.ColorTemperature.MirekSchema.MirekMaximum,
  213. }
  214. }
  215. if svc.Color != nil {
  216. hwState.SupportFlags |= device.SFlagColor
  217. hwState.ColorFlags |= device.CFlagXY
  218. hwState.ColorGamut = gentools.Ptr(svc.Color.Gamut)
  219. hwState.ColorGamut.Label = svc.Color.GamutType
  220. }
  221. }
  222. }
  223. if buttonCount == 4 {
  224. hwState.Buttons = []string{"On", "DimUp", "DimDown", "Off"}
  225. } else if buttonCount == 1 {
  226. hwState.Buttons = []string{"Button"}
  227. } else {
  228. for n := 1; n <= buttonCount; n++ {
  229. hwState.Buttons = append(hwState.Buttons, fmt.Sprint("Button", n))
  230. }
  231. }
  232. return hwState, hwMeta
  233. }
  234. func (res *ResourceData) GenerateState(resources map[string]*ResourceData) device.State {
  235. state := device.State{}
  236. for _, ptr := range res.Services {
  237. switch ptr.Kind {
  238. case "light":
  239. light := resources[ptr.ID]
  240. if light == nil {
  241. continue
  242. }
  243. if light.Power != nil {
  244. state.Power = gentools.Ptr(light.Power.On)
  245. }
  246. if light.Dimming != nil {
  247. state.Intensity = gentools.Ptr(light.Dimming.Brightness / 100.0)
  248. }
  249. if light.ColorTemperature != nil {
  250. if light.ColorTemperature.Mirek != nil {
  251. state.Color = &color.Color{K: gentools.Ptr(1000000 / *light.ColorTemperature.Mirek)}
  252. }
  253. }
  254. if light.Color != nil {
  255. if state.Color == nil || state.Color.IsEmpty() {
  256. state.Color = &color.Color{XY: &light.Color.XY}
  257. }
  258. }
  259. }
  260. }
  261. return state
  262. }
  263. type SensorButton struct {
  264. LastEvent string `json:"last_event"`
  265. }
  266. type SensorMotion struct {
  267. Motion bool `json:"motion"`
  268. Valid bool `json:"motion_valid"`
  269. }
  270. type SensorTemperature struct {
  271. Temperature float64 `json:"temperature"`
  272. Valid bool `json:"temperature_valid"`
  273. }
  274. type PowerState struct {
  275. BatteryState string `json:"battery_state"`
  276. BatteryLevel float64 `json:"battery_level"`
  277. }
  278. type LightPower struct {
  279. On bool `json:"on"`
  280. }
  281. type LightDimming struct {
  282. Brightness float64 `json:"brightness"`
  283. }
  284. type LightColor struct {
  285. Gamut color.Gamut `json:"gamut"`
  286. GamutType string `json:"gamut_type"`
  287. XY color.XY `json:"xy"`
  288. }
  289. type LightCT struct {
  290. Mirek *int `json:"mirek"`
  291. MirekSchema LightCTMirekSchema `json:"mirek_schema"`
  292. MirekValid bool `json:"mirek_valid"`
  293. }
  294. type LightCTMirekSchema struct {
  295. MirekMaximum int `json:"mirek_maximum"`
  296. MirekMinimum int `json:"mirek_minimum"`
  297. }
  298. type LightDynamics struct {
  299. Speed float64 `json:"speed"`
  300. SpeedValid bool `json:"speed_valid"`
  301. Status string `json:"status"`
  302. StatusValues []string `json:"status_values"`
  303. }
  304. type LightAlert struct {
  305. ActionValues []string `json:"action_values"`
  306. }
  307. type ResourceUpdate struct {
  308. Name *string
  309. Power *bool
  310. ColorXY *color.XY
  311. Brightness *float64
  312. Mirek *int
  313. TransitionDuration *time.Duration
  314. }
  315. func (r ResourceUpdate) MarshalJSON() ([]byte, error) {
  316. chunks := make([]string, 0, 4)
  317. if r.Name != nil {
  318. s, _ := json.Marshal(*r.Name)
  319. chunks = append(chunks, fmt.Sprintf(`"metadata":{"name":%s}`, string(s)))
  320. }
  321. if r.Power != nil {
  322. chunks = append(chunks, fmt.Sprintf(`"on":{"on":%v}`, *r.Power))
  323. }
  324. if r.ColorXY != nil {
  325. chunks = append(chunks, fmt.Sprintf(`"color":{"xy":{"x":%f,"y":%f}}`, r.ColorXY.X, r.ColorXY.Y))
  326. }
  327. if r.Brightness != nil {
  328. chunks = append(chunks, fmt.Sprintf(`"dimming":{"brightness":%f}`, *r.Brightness))
  329. }
  330. if r.Mirek != nil {
  331. chunks = append(chunks, fmt.Sprintf(`"color_temperature":{"mirek":%d}`, *r.Mirek))
  332. }
  333. if r.TransitionDuration != nil {
  334. chunks = append(chunks, fmt.Sprintf(`"dynamics":{"duration":%d}`, r.TransitionDuration.Truncate(time.Millisecond*100).Milliseconds()))
  335. }
  336. return []byte(fmt.Sprintf("{%s}", strings.Join(chunks, ","))), nil
  337. }
  338. type ResourceLink struct {
  339. ID string `json:"rid"`
  340. Kind string `json:"rtype"`
  341. }
  342. func (rl *ResourceLink) Path() string {
  343. return fmt.Sprintf("/clip/v2/resource/%s/%s", rl.Kind, rl.ID)
  344. }
  345. type CreateUserInput struct {
  346. DeviceType string `json:"devicetype"`
  347. }
  348. type CreateUserResponse struct {
  349. Success *struct {
  350. Username string `json:"username"`
  351. } `json:"success"`
  352. Error *struct {
  353. Type int `json:"type"`
  354. Address string `json:"address"`
  355. Description string `json:"description"`
  356. } `json:"error"`
  357. }
  358. type DiscoveryEntry struct {
  359. Id string `json:"id"`
  360. InternalIPAddress string `json:"internalipaddress"`
  361. }
  362. type BridgeDeviceInfo struct {
  363. XMLName xml.Name `xml:"root"`
  364. Text string `xml:",chardata"`
  365. Xmlns string `xml:"xmlns,attr"`
  366. SpecVersion struct {
  367. Text string `xml:",chardata"`
  368. Major string `xml:"major"`
  369. Minor string `xml:"minor"`
  370. } `xml:"specVersion"`
  371. URLBase string `xml:"URLBase"`
  372. Device struct {
  373. Text string `xml:",chardata"`
  374. DeviceType string `xml:"deviceType"`
  375. FriendlyName string `xml:"friendlyName"`
  376. Manufacturer string `xml:"manufacturer"`
  377. ManufacturerURL string `xml:"manufacturerURL"`
  378. ModelDescription string `xml:"modelDescription"`
  379. ModelName string `xml:"modelName"`
  380. ModelNumber string `xml:"modelNumber"`
  381. ModelURL string `xml:"modelURL"`
  382. SerialNumber string `xml:"serialNumber"`
  383. UDN string `xml:"UDN"`
  384. PresentationURL string `xml:"presentationURL"`
  385. IconList struct {
  386. Text string `xml:",chardata"`
  387. Icon struct {
  388. Text string `xml:",chardata"`
  389. Mimetype string `xml:"mimetype"`
  390. Height string `xml:"height"`
  391. Width string `xml:"width"`
  392. Depth string `xml:"depth"`
  393. URL string `xml:"url"`
  394. } `xml:"icon"`
  395. } `xml:"iconList"`
  396. } `xml:"device"`
  397. }