package models import ( "encoding/json" "errors" "fmt" "regexp" "strings" "time" "github.com/google/uuid" "github.com/tecamino/tecamino-dbm/utils" ws "github.com/tecamino/tecamino-dbm/websocket" wsModels "github.com/tecamino/tecamino-dbm/websocket/models" json_data "github.com/tecamino/tecamino-json_data" json_dataModels "github.com/tecamino/tecamino-json_data/models" ) const ( OnCreate = "onCreate" OnChange = "onChange" OnDelete = "onDelete" ) type Datapoint struct { Uuid uuid.UUID `json:"uuid"` Value any `json:"value,omitempty"` Path string `json:"path"` CreateDateTime int64 `json:"createDateTime,omitempty"` UpdateDateTime int64 `json:"updateDateTime,omitempty"` Datapoints map[string]*Datapoint `json:"-"` Drivers json_dataModels.Drivers `json:"drivers,omitempty"` Subscriptions Subscriptions `json:"-"` Type json_dataModels.Type `json:"type"` ReadWrite json_dataModels.Rights `json:"readWrite"` HasChild bool `json:"-"` } func (d *Datapoint) Set(path string, set json_dataModels.Set) (bool, error) { var changed bool if path != "" { changed = true d.Path = path } if set.Type != "" { changed = true d.Type = set.Type } if d.Type != "" { if d.Value == nil && set.Value == nil { changed = true d.Value = d.Type.DefaultValue() } else if d.Value != d.Type.ConvertValue(set.Value) { changed = true d.Value = d.Type.ConvertValue(set.Value) } } if set.Rights != "" { changed = true d.ReadWrite = set.Rights.GetRights() } if changed { d.UpdateDateTime = time.Now().UnixMilli() } if set.Driver == nil { return changed, nil } if set.Driver.Type == "" { return changed, errors.New("driver type missing") } if set.Driver.Bus == "" { return changed, errors.New("driver bus name missing") } if d.Drivers == nil { d.Drivers = json_dataModels.NewDrivers() } d.Drivers.AddDriver(set.Driver.Type, set.Driver.Bus, set.Driver.Address) d.UpdateDateTime = time.Now().UnixMilli() return changed, nil } func (d *Datapoint) GetValueUint64() uint64 { return utils.Uint64From(d.Value) } func (d *Datapoint) CreateDatapoints(uuids *Uuids, sets ...json_dataModels.Set) (created []json_dataModels.Set, err error) { if len(sets) == 0 { return } publishes := []json_dataModels.Publish{} for _, dp := range sets { parts := strings.Split(dp.Path, ":") current := d for i, part := range parts { if current.Datapoints == nil { current.Datapoints = make(map[string]*Datapoint) } if i == len(parts)-1 { // Leaf node: create or update datapoint if existing, ok := current.Datapoints[part]; ok { _, err := existing.Set("", dp) if err != nil { return nil, err } created = append(created, json_dataModels.Set{ Uuid: existing.Uuid, Path: existing.Path, Type: existing.Type, Value: existing.Value, Rights: existing.ReadWrite, Drivers: &existing.Drivers, Updated: true, HasChild: existing.HasChild, }) existing.Publish(OnChange) } else { var uid uuid.UUID = uuid.New() if dp.Uuid != uuid.Nil { uid = dp.Uuid } ndp := &Datapoint{ Uuid: uid, CreateDateTime: time.Now().UnixMilli(), Subscriptions: InitSubscribtion(), } // Create new current.Datapoints[part] = ndp _, err := ndp.Set(strings.Join(parts, ":"), dp) if err != nil { return nil, err } //add uuid to flat map for faster lookup uuids.AddDatapoint(current, ndp) created = append(created, json_dataModels.Set{ Uuid: ndp.Uuid, Path: ndp.Path, Type: ndp.Type, Value: ndp.Value, Rights: ndp.ReadWrite, Driver: dp.Driver, HasChild: ndp.HasChild, }) publishes = append(publishes, json_dataModels.Publish{ Event: OnCreate, Uuid: ndp.Uuid, Path: ndp.Path, Type: ndp.Type, Value: ndp.Value, }) } } // Traverse or create intermediate datapoints if next, ok := current.Datapoints[part]; ok { current = next } else { newDp := &Datapoint{ Uuid: uuid.New(), Path: strings.Join(parts[:i+1], ":"), Type: json_dataModels.NONE, CreateDateTime: time.Now().UnixMilli(), UpdateDateTime: time.Now().UnixMilli(), Subscriptions: InitSubscribtion(), } //add uuid to flat map for faster lookup uuids.AddDatapoint(current, newDp) created = append(created, json_dataModels.Set{ Uuid: newDp.Uuid, Path: newDp.Path, Type: newDp.Type, Value: newDp.Value, Rights: newDp.ReadWrite, HasChild: newDp.HasChild, }) if dp.Rights != "" { newDp.ReadWrite = dp.Rights.GetRights() } publishes = append(publishes, json_dataModels.Publish{ Event: OnCreate, Uuid: newDp.Uuid, Path: newDp.Path, Type: newDp.Type, Value: newDp.Value, }) current.Datapoints[part] = newDp current = newDp } } } r := json_data.NewResponse() r.Publish = append(r.Publish, publishes...) b, err := json.Marshal(r) if err != nil { return created, err } ws.SendBroadcast(b) return } func (d *Datapoint) ImportDatapoint(uuids *Uuids, dp *Datapoint, path string) (err error) { parts := strings.Split(dp.Path, ":") current := d for i, part := range parts { if current.Datapoints == nil { current.Datapoints = make(map[string]*Datapoint) } if i == len(parts)-1 { // Leaf node: import the datapoint if existing, ok := current.Datapoints[part]; ok { existing.Type = dp.Type existing.Value = dp.Type.ConvertValue(dp.Value) existing.ReadWrite = dp.ReadWrite.GetRights() existing.UpdateDateTime = time.Now().UnixMilli() dp.Publish(OnChange) } else { dp.Path = strings.Join(parts, ":") dp.ReadWrite = dp.ReadWrite.GetRights() dp.UpdateDateTime = time.Now().UnixMilli() dp.Subscriptions = InitSubscribtion() current.Datapoints[part] = dp //add uuid to flat map for faster lookup uuids.AddDatapoint(current, dp) dp.Publish(OnChange) } return nil } // Traverse or create intermediate nodes if next, ok := current.Datapoints[part]; ok { current = next } else { newDp := &Datapoint{ Path: strings.Join(parts[:i+1], ":"), Type: json_dataModels.NONE, ReadWrite: dp.ReadWrite.GetRights(), UpdateDateTime: time.Now().UnixMilli(), } newDp.ReadWrite = newDp.ReadWrite.GetRights() current.Datapoints[part] = newDp current = newDp //add uuid to flat map for faster lookup uuids.AddDatapoint(current, newDp) } } return nil } func (d *Datapoint) UpdateDatapointValue(value any, path string) error { paths := strings.Split(path, ":") current := d for i, part := range paths { dp, ok := current.Datapoints[part] if !ok { return fmt.Errorf("datapoint path not found: %s (at %s)", path, part) } if i == len(paths)-1 { dp.Value = dp.Type.ConvertValue(value) dp.UpdateDateTime = time.Now().UnixMilli() dp.Publish(OnChange) return nil } current = dp } return nil } func (d *Datapoint) UpdateValue(value any) error { d.Value = d.Type.ConvertValue(value) d.UpdateDateTime = time.Now().UnixMilli() d.Publish(OnChange) return nil } func (d *Datapoint) RemoveDatapoint(set json_dataModels.Set) (sets []json_dataModels.Set, err error) { parts := strings.Split(set.Path, ":") if len(parts) < 1 { return sets, fmt.Errorf("invalid path: '%s'", set.Path) } publishes := []json_dataModels.Publish{} current := d for i := 0; i < len(parts)-1; i++ { next, ok := current.Datapoints[parts[i]] if !ok { return sets, fmt.Errorf("path not found: '%s'", strings.Join(parts[:i+1], ":")) } current = next } toDelete := parts[len(parts)-1] if dp, ok := current.Datapoints[toDelete]; ok { s, p := removeChildren(dp) sets = append(sets, s...) publishes = append(publishes, p...) publishes = append(publishes, json_dataModels.Publish{ Event: OnDelete, Uuid: dp.Uuid, Path: dp.Path, }) sets = append(sets, json_dataModels.Set{ Uuid: dp.Uuid, Path: dp.Path, }) delete(current.Datapoints, toDelete) r := json_data.NewResponse() r.Publish = append(r.Publish, publishes...) b, err := json.Marshal(r) if err != nil { return sets, err } ws.SendBroadcast(b) return sets, nil } return sets, fmt.Errorf("datapoint '%s' not found", set.Path) } // removes all children and grandchlidren of datapoint func removeChildren(dp *Datapoint) (sets []json_dataModels.Set, pubs []json_dataModels.Publish) { for name, d := range dp.Datapoints { s, p := removeChildren(d) sets = append(sets, s...) pubs = append(pubs, p...) sets = append(sets, json_dataModels.Set{ Uuid: d.Uuid, Path: d.Path, }) delete(d.Datapoints, name) } return sets, pubs } func (d *Datapoint) GetAllDatapoints(depth uint) (dps Datapoints) { dps = append(dps, d) if depth == 1 { return } else if depth > 0 { depth-- } var dfs func(dp *Datapoint, currentDepth uint) dfs = func(dp *Datapoint, currentDepth uint) { switch depth { case 0: // Return all descendants case currentDepth: return } for _, child := range dp.Datapoints { dps = append(dps, child) dfs(child, currentDepth+1) } } dfs(d, 0) dps.SortSlice() return } func (d *Datapoint) QueryDatapoints(depth uint, path string) (dps Datapoints) { parts := strings.Split(path, ":") var dfs func(current *Datapoint, index int) dfs = func(current *Datapoint, index int) { if index == len(parts) || path == "" { dps = append(dps, current.GetAllDatapoints(depth)...) return } pattern := "^" + parts[index] + "$" re, err := regexp.Compile(pattern) if err != nil { return } for name, dp := range current.Datapoints { if re.MatchString(name) { dfs(dp, index+1) } } } dfs(d, 0) dps.SortSlice() return } func (d *Datapoint) AddSubscribtion(conn *wsModels.Client, sub json_dataModels.Subscription) { if d.Subscriptions == nil { return } if s, ok := d.Subscriptions[conn]; ok { s.OnCreate = sub.OnCreate s.OnChange = sub.OnChange s.OnDelete = sub.OnDelete } else { d.Subscriptions[conn] = &Subscription{ OnCreate: sub.OnCreate, OnChange: sub.OnChange, OnDelete: sub.OnDelete, } } } func (d *Datapoint) RenamePaths(oldPath string) { visited := make(map[*Datapoint]bool) if len(d.Datapoints) == 0 { return } for _, dp := range d.Datapoints { dp.Path = strings.Replace(dp.Path, oldPath, d.Path, 1) dp.renameSubPaths(oldPath, d.Path, visited) } } func (d *Datapoint) renameSubPaths(oldPath, newPath string, visited map[*Datapoint]bool) { if visited[d] { return } visited[d] = true if len(d.Datapoints) == 0 { return } for _, dp := range d.Datapoints { dp.Path = strings.Replace(dp.Path, oldPath, newPath, 1) dp.renameSubPaths(oldPath, newPath, visited) } } func (d *Datapoint) RemoveSubscribtion(client *wsModels.Client) { delete(d.Subscriptions, client) } func (d *Datapoint) Publish(eventType string) error { r := json_data.NewResponse() r.AddPublish(json_dataModels.Publish{ Event: eventType, Uuid: d.Uuid, Path: d.Path, Type: d.Type, Value: d.Value, }) b, err := json.Marshal(r) if err != nil { return err } switch eventType { case OnCreate, OnDelete: ws.SendBroadcast(b) default: for client := range d.Subscriptions { client.SendResponse(b) } } return nil }