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.

368 lines
9.5 KiB

  1. package mysql
  2. import (
  3. "context"
  4. "encoding/json"
  5. "git.aiterp.net/lucifer/new-server/models"
  6. sq "github.com/Masterminds/squirrel"
  7. "github.com/jmoiron/sqlx"
  8. "log"
  9. "strings"
  10. )
  11. type deviceRecord struct {
  12. ID int `db:"id"`
  13. BridgeID int `db:"bridge_id"`
  14. InternalID string `db:"internal_id"`
  15. Icon string `db:"icon"`
  16. Name string `db:"name"`
  17. Capabilities string `db:"capabilities"`
  18. ButtonNames string `db:"button_names"`
  19. }
  20. type deviceStateRecord struct {
  21. DeviceID int `db:"device_id"`
  22. Hue float64 `db:"hue"`
  23. Saturation float64 `db:"saturation"`
  24. Kelvin int `db:"kelvin"`
  25. Power bool `db:"power"`
  26. Intensity float64 `db:"intensity"`
  27. }
  28. type devicePropertyRecord struct {
  29. DeviceID int `db:"device_id"`
  30. Key string `db:"prop_key"`
  31. Value string `db:"prop_value"`
  32. IsUser bool `db:"is_user"`
  33. }
  34. type deviceTagRecord struct {
  35. DeviceID int `db:"device_id"`
  36. TagName string `db:"tag_name"`
  37. }
  38. type DeviceRepo struct {
  39. DBX *sqlx.DB
  40. }
  41. func (r *DeviceRepo) Find(ctx context.Context, id int) (*models.Device, error) {
  42. var device deviceRecord
  43. err := r.DBX.GetContext(ctx, &device, "SELECT * FROM device WHERE id = ?", id)
  44. if err != nil {
  45. return nil, dbErr(err)
  46. }
  47. return r.populateOne(ctx, device)
  48. }
  49. func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.ReferenceKind, value string) ([]models.Device, error) {
  50. q := sq.Select("device.*").From("device")
  51. var requiredTags []string
  52. var unwantedTags []string
  53. switch kind {
  54. case models.RKDeviceID:
  55. q = q.Where(sq.Eq{"id": strings.Split(value, ",")})
  56. case models.RKBridgeID:
  57. q = q.Where(sq.Eq{"bridge_id": strings.Split(value, ",")})
  58. case models.RKTag:
  59. allTags := strings.Split(strings.ReplaceAll(strings.ReplaceAll(value, "-", ",-"), "+", ",+"), ",")
  60. optionalTags := make([]string, 0, len(allTags))
  61. for _, tag := range allTags {
  62. if tag == "" || tag == "+" || tag == "-" {
  63. continue
  64. }
  65. if strings.HasPrefix(tag, "+") {
  66. requiredTags = append(requiredTags, tag[1:])
  67. } else if strings.HasPrefix(tag, "-") {
  68. unwantedTags = append(unwantedTags, tag[1:])
  69. } else {
  70. optionalTags = append(optionalTags, tag)
  71. }
  72. }
  73. q = q.Join("device_tag dt ON device.id=dt.device_id").Where(sq.Eq{"dt.tag_name": optionalTags})
  74. case models.RKAll:
  75. default:
  76. log.Println("Unknown reference kind used for device fetch:", kind)
  77. return []models.Device{}, nil
  78. }
  79. query, args, err := q.ToSql()
  80. if err != nil {
  81. return nil, dbErr(err)
  82. }
  83. records := make([]deviceRecord, 0, 8)
  84. err = r.DBX.SelectContext(ctx, &records, query, args...)
  85. if err != nil {
  86. return nil, dbErr(err)
  87. }
  88. if len(requiredTags) > 0 || len(unwantedTags) > 0 {
  89. return r.populateFiltered(ctx, records, requiredTags, unwantedTags)
  90. }
  91. return r.populate(ctx, records)
  92. }
  93. func (r *DeviceRepo) Save(ctx context.Context, device *models.Device) error {
  94. tx, err := r.DBX.Beginx()
  95. if err != nil {
  96. return dbErr(err)
  97. }
  98. defer tx.Rollback()
  99. record := deviceRecord{
  100. ID: device.ID,
  101. BridgeID: device.BridgeID,
  102. InternalID: device.InternalID,
  103. Icon: device.Icon,
  104. Name: device.Name,
  105. Capabilities: strings.Join(models.DeviceCapabilitiesToStrings(device.Capabilities), ","),
  106. ButtonNames: strings.Join(device.ButtonNames, ","),
  107. }
  108. if device.ID > 0 {
  109. _, err := tx.NamedExecContext(ctx, `
  110. UPDATE device SET
  111. internal_id = :internal_id,
  112. icon = :icon,
  113. name = :name,
  114. capabilities = :capabilities,
  115. button_names = :button_names
  116. WHERE id=:id
  117. `, record)
  118. if err != nil {
  119. return dbErr(err)
  120. }
  121. // Let's just be lazy for now, optimize later if need be.
  122. _, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
  123. if err != nil {
  124. return dbErr(err)
  125. }
  126. _, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID)
  127. if err != nil {
  128. return dbErr(err)
  129. }
  130. } else {
  131. res, err := tx.NamedExecContext(ctx, `
  132. INSERT INTO device (bridge_id, internal_id, icon, name, capabilities, button_names)
  133. VALUES (:bridge_id, :internal_id, :icon, :name, :capabilities, :button_names)
  134. `, record)
  135. if err != nil {
  136. return dbErr(err)
  137. }
  138. lastID, err := res.LastInsertId()
  139. if err != nil {
  140. return dbErr(err)
  141. }
  142. record.ID = int(lastID)
  143. device.ID = int(lastID)
  144. }
  145. for _, tag := range device.Tags {
  146. _, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
  147. if err != nil {
  148. return dbErr(err)
  149. }
  150. }
  151. for key, value := range device.UserProperties {
  152. _, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)",
  153. record.ID, key, value,
  154. )
  155. if err != nil {
  156. return dbErr(err)
  157. }
  158. }
  159. for key, value := range device.DriverProperties {
  160. j, err := json.Marshal(value)
  161. if err != nil {
  162. // Eh, it'll get filled by the driver anyway
  163. continue
  164. }
  165. _, err = tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 0)",
  166. record.ID, key, string(j),
  167. )
  168. if err != nil {
  169. // Return err here anyway, it might put the tx in a bad state to ignore it.
  170. return dbErr(err)
  171. }
  172. }
  173. _, err = tx.NamedExecContext(ctx, `
  174. REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
  175. VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
  176. `, deviceStateRecord{
  177. DeviceID: record.ID,
  178. Hue: device.State.Color.Hue,
  179. Saturation: device.State.Color.Saturation,
  180. Kelvin: device.State.Color.Kelvin,
  181. Power: device.State.Power,
  182. Intensity: device.State.Intensity,
  183. })
  184. if err != nil {
  185. return dbErr(err)
  186. }
  187. return tx.Commit()
  188. }
  189. func (r *DeviceRepo) Delete(ctx context.Context, device *models.Device) error {
  190. _, err := r.DBX.ExecContext(ctx, "DELETE FROM device WHERE Id=?", device.ID)
  191. if err != nil {
  192. return dbErr(err)
  193. }
  194. return nil
  195. }
  196. func (r *DeviceRepo) populateOne(ctx context.Context, record deviceRecord) (*models.Device, error) {
  197. records, err := r.populate(ctx, []deviceRecord{record})
  198. if err != nil {
  199. return nil, err
  200. }
  201. return &records[0], nil
  202. }
  203. func (r *DeviceRepo) populateFiltered(ctx context.Context, records []deviceRecord, requiredTags []string, unwantedTags []string) ([]models.Device, error) {
  204. devices, err := r.populate(ctx, records)
  205. if err != nil {
  206. return nil, err
  207. }
  208. filteredDevices := make([]models.Device, 0, len(devices))
  209. for _, device := range devices {
  210. if device.HasTag(unwantedTags...) {
  211. continue
  212. }
  213. if len(requiredTags) == 0 || device.HasTag(requiredTags...) {
  214. filteredDevices = append(filteredDevices, device)
  215. }
  216. }
  217. return filteredDevices, nil
  218. }
  219. func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]models.Device, error) {
  220. if len(records) == 0 {
  221. return []models.Device{}, nil
  222. }
  223. ids := make([]int, 0, len(records))
  224. for _, record := range records {
  225. ids = append(ids, record.ID)
  226. }
  227. tagsQuery, tagsArgs, err := sq.Select("*").From("device_tag").Where(sq.Eq{"device_id": ids}).ToSql()
  228. if err != nil {
  229. return nil, dbErr(err)
  230. }
  231. propsQuery, propsArgs, err := sq.Select("*").From("device_property").Where(sq.Eq{"device_id": ids}).ToSql()
  232. if err != nil {
  233. return nil, dbErr(err)
  234. }
  235. stateQuery, stateArgs, err := sq.Select("*").From("device_state").Where(sq.Eq{"device_id": ids}).ToSql()
  236. if err != nil {
  237. return nil, dbErr(err)
  238. }
  239. states := make([]deviceStateRecord, 0, len(records))
  240. props := make([]devicePropertyRecord, 0, len(records)*8)
  241. tags := make([]deviceTagRecord, 0, len(records)*4)
  242. err = r.DBX.SelectContext(ctx, &states, stateQuery, stateArgs...)
  243. if err != nil {
  244. return nil, dbErr(err)
  245. }
  246. err = r.DBX.SelectContext(ctx, &props, propsQuery, propsArgs...)
  247. if err != nil {
  248. return nil, dbErr(err)
  249. }
  250. err = r.DBX.SelectContext(ctx, &tags, tagsQuery, tagsArgs...)
  251. if err != nil {
  252. return nil, dbErr(err)
  253. }
  254. devices := make([]models.Device, 0, len(records))
  255. for _, record := range records {
  256. device := models.Device{
  257. ID: record.ID,
  258. BridgeID: record.BridgeID,
  259. InternalID: record.InternalID,
  260. Icon: record.Icon,
  261. Name: record.Name,
  262. ButtonNames: strings.Split(record.ButtonNames, ","),
  263. DriverProperties: make(map[string]interface{}, 8),
  264. UserProperties: make(map[string]string, 8),
  265. Tags: make([]string, 0, 8),
  266. }
  267. if device.ButtonNames[0] == "" {
  268. device.ButtonNames = device.ButtonNames[:0]
  269. }
  270. caps := make([]models.DeviceCapability, 0, 16)
  271. for _, capStr := range strings.Split(record.Capabilities, ",") {
  272. caps = append(caps, models.DeviceCapability(capStr))
  273. }
  274. device.Capabilities = caps
  275. for _, state := range states {
  276. if state.DeviceID == record.ID {
  277. device.State = models.DeviceState{
  278. Power: state.Power,
  279. Color: models.ColorValue{
  280. Hue: state.Hue,
  281. Saturation: state.Saturation,
  282. Kelvin: state.Kelvin,
  283. },
  284. Intensity: state.Intensity,
  285. }
  286. }
  287. }
  288. driverProps := make(map[string]json.RawMessage, 8)
  289. for _, prop := range props {
  290. if prop.DeviceID == record.ID {
  291. if prop.IsUser {
  292. device.UserProperties[prop.Key] = prop.Value
  293. } else {
  294. driverProps[prop.Key] = json.RawMessage(prop.Value)
  295. }
  296. }
  297. }
  298. if len(driverProps) > 0 {
  299. j, err := json.Marshal(driverProps)
  300. if err != nil {
  301. return nil, dbErr(err)
  302. }
  303. err = json.Unmarshal(j, &device.DriverProperties)
  304. if err != nil {
  305. return nil, dbErr(err)
  306. }
  307. }
  308. for _, tag := range tags {
  309. if tag.DeviceID == record.ID {
  310. device.Tags = append(device.Tags, tag.TagName)
  311. }
  312. }
  313. devices = append(devices, device)
  314. }
  315. return devices, nil
  316. }