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.

457 lines
12 KiB

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