From 8fe185055157a91622ff9c22c3bd040f0620db6e Mon Sep 17 00:00:00 2001 From: Braden Date: Wed, 25 May 2022 16:35:54 -0700 Subject: [PATCH 01/18] Hexagon architecture refactoring. Initial work done for user and module. Services and handlers not withstanding. --- internal/controller/controller.go | 1 + internal/controller/polybuffer.go | 2 +- internal/core/domain/device.go | 23 ++ internal/core/domain/module.go | 31 +++ internal/core/domain/persistent.go | 13 ++ internal/core/domain/user.go | 33 +++ internal/models/module.go | 1 + internal/models/zone.go | 2 +- internal/modules/module/module.go | 3 + internal/modules/module/repository.go | 62 ++++++ internal/modules/module/service.go | 50 +++++ internal/modules/user/repository.go | 62 ++++++ internal/modules/user/service.go | 80 +++++++ internal/modules/user/user.go | 14 ++ internal/port/rest/user.go | 3 + internal/server/endpoints.go | 14 +- internal/server/modules.go | 83 ++++++-- modules/spotify/spotify.go | 4 +- modules/squid/squid.go | 294 +++++++++++++++----------- modules/weather/weather.go | 14 +- modules/webstats/webstats.go | 57 +++++ 21 files changed, 686 insertions(+), 160 deletions(-) create mode 100644 internal/core/domain/device.go create mode 100644 internal/core/domain/module.go create mode 100644 internal/core/domain/persistent.go create mode 100644 internal/core/domain/user.go create mode 100644 internal/modules/module/module.go create mode 100644 internal/modules/module/repository.go create mode 100644 internal/modules/module/service.go create mode 100644 internal/modules/user/repository.go create mode 100644 internal/modules/user/service.go create mode 100644 internal/modules/user/user.go create mode 100644 internal/port/rest/user.go create mode 100644 modules/webstats/webstats.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index d1ee229..f102b93 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -22,6 +22,7 @@ type Controller struct { func NewController() (*Controller, error) { c := &Controller{} + c.Entities = LoadEntities() c.Modules = LoadModules() c.Attributes = LoadAttributes() diff --git a/internal/controller/polybuffer.go b/internal/controller/polybuffer.go index a3e90bc..9697557 100644 --- a/internal/controller/polybuffer.go +++ b/internal/controller/polybuffer.go @@ -17,7 +17,7 @@ type Observer struct { type Mutation struct { Key string - Value interface{} + Value any } type Observable struct { diff --git a/internal/core/domain/device.go b/internal/core/domain/device.go new file mode 100644 index 0000000..e7ef1e3 --- /dev/null +++ b/internal/core/domain/device.go @@ -0,0 +1,23 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type Device struct { + Persistent + NetworkId string `json:"networkId" gorm:"-"` + EntityId string `json:"entityId" gorm:"-"` + Name string `json:"name"` + Hostname string `json:"hostname"` + Mac string `json:"mac"` + Ipv4 string `json:"ipv4"` + Ipv6 string `json:"ipv6"` +} + +type DeviceRepository interface { + FindAll() ([]*Device, error) + FindById(id string) (*Device, error) + Create(*Device) error + FindOrCreate(*Device) error + Update(*Device) error + Delete(*Device) error +} diff --git a/internal/core/domain/module.go b/internal/core/domain/module.go new file mode 100644 index 0000000..cb17364 --- /dev/null +++ b/internal/core/domain/module.go @@ -0,0 +1,31 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type Module struct { + Persistent + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Description string `json:"description"` + Version string `json:"version"` + Author string `json:"author"` + Channel chan Module `json:"-" gorm:"-"` + State string `json:"state"` + Enabled bool `json:"enabled" gorm:"default:true"` + Recover int `json:"recover"` +} + +type ModuleRepository interface { + FindAll() ([]*Module, error) + FindByName(name string) (*Module, error) +} + +type ModuleService interface { + FindAll() ([]*Module, error) + FindByName(name string) (*Module, error) + Disable(name string) error + Enable(name string) error + Reload(name string) error + Halt(name string) error +} diff --git a/internal/core/domain/persistent.go b/internal/core/domain/persistent.go new file mode 100644 index 0000000..b932ce1 --- /dev/null +++ b/internal/core/domain/persistent.go @@ -0,0 +1,13 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +import "time" + +type Persistent struct { + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` + Deleted bool `json:"deleted"` + deletedAt *time.Time `sql:"index"` + Id string `json:"id" gorm:"primary_key;type:string;default:uuid_generate_v4()"` +} diff --git a/internal/core/domain/user.go b/internal/core/domain/user.go new file mode 100644 index 0000000..ea8e273 --- /dev/null +++ b/internal/core/domain/user.go @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type User struct { + Persistent + Username string `json:"username"` + First string `json:"first"` + Middle string `json:"middle"` + Last string `json:"last"` + Type string `json:"type"` + Password string `json:"password"` +} + +type UserRepository interface { + FindAll() ([]*User, error) + FindById(id string) (*User, error) + Create(*User) error + FindOrCreate(*User) error + Update(*User) error + Delete(*User) error +} + +type UserService interface { + Register(*User) error + Authenticate(*User) error + FindAll() ([]*User, error) + FindById(id string) (*User, error) + Create(*User) error + FindOrCreate(*User) error + Update(*User) error + Delete(*User) error +} diff --git a/internal/models/module.go b/internal/models/module.go index 564eb9f..a3367b8 100644 --- a/internal/models/module.go +++ b/internal/models/module.go @@ -20,6 +20,7 @@ type Module struct { Channel chan Module `json:"-" gorm:"-"` State string `json:"state"` Enabled bool `json:"enabled" gorm:"default:true"` + Recover int `json:"recover"` } // create inserts the current module into the database diff --git a/internal/models/zone.go b/internal/models/zone.go index eb912d0..3eaa3b3 100644 --- a/internal/models/zone.go +++ b/internal/models/zone.go @@ -17,7 +17,7 @@ type Zone struct { // Emplace will Find or Create a zone based on its id. func (z *Zone) Emplace() (err error) { z.UpdatedAt = time.Now() - err = store.DB.Model(&Zone{}).Where("id = ?", z.Id).FirstOrCreate(z).Error + err = store.DB.Model(&Zone{}).FirstOrCreate(z).Error if err != nil { return err } diff --git a/internal/modules/module/module.go b/internal/modules/module/module.go new file mode 100644 index 0000000..e25e588 --- /dev/null +++ b/internal/modules/module/module.go @@ -0,0 +1,3 @@ +// Copyright (c) 2022 Braden Nicholson + +package module diff --git a/internal/modules/module/repository.go b/internal/modules/module/repository.go new file mode 100644 index 0000000..d3a85b5 --- /dev/null +++ b/internal/modules/module/repository.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Braden Nicholson + +package module + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type moduleRepo struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) domain.UserRepository { + return &moduleRepo{ + db: db, + } +} + +func (u moduleRepo) FindAll() ([]*domain.User, error) { + var target []*domain.User + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u moduleRepo) FindById(id string) (*domain.User, error) { + var target *domain.User + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u moduleRepo) Create(user *domain.User) error { + if err := u.db.Create(user).Error; err != nil { + return err + } + return nil +} + +func (u moduleRepo) FindOrCreate(user *domain.User) error { + if err := u.db.FirstOrCreate(user).Error; err != nil { + return err + } + return nil +} + +func (u moduleRepo) Update(user *domain.User) error { + if err := u.db.Save(user).Error; err != nil { + return err + } + return nil +} + +func (u moduleRepo) Delete(user *domain.User) error { + if err := u.db.Delete(user).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/module/service.go b/internal/modules/module/service.go new file mode 100644 index 0000000..052364e --- /dev/null +++ b/internal/modules/module/service.go @@ -0,0 +1,50 @@ +// Copyright (c) 2022 Braden Nicholson + +package module + +import ( + "udap/internal/core/domain" +) + +type moduleService struct { + repository domain.ModuleRepository +} + +func NewModuleService(repository domain.ModuleRepository) domain.ModuleService { + return moduleService{repository: repository} +} + +// Repository Mapping + +func (u moduleService) FindAll() ([]*domain.Module, error) { + return u.repository.FindAll() +} + +func (u moduleService) FindByName(name string) (*domain.Module, error) { + return u.repository.FindByName(name) +} + +func (u moduleService) FindById(id string) (*domain.Module, error) { + // TODO implement me + panic("implement me") +} + +func (u moduleService) Disable(name string) error { + // TODO implement me + panic("implement me") +} + +func (u moduleService) Enable(name string) error { + // TODO implement me + panic("implement me") +} + +func (u moduleService) Reload(name string) error { + // TODO implement me + panic("implement me") +} + +func (u moduleService) Halt(name string) error { + // TODO implement me + panic("implement me") +} diff --git a/internal/modules/user/repository.go b/internal/modules/user/repository.go new file mode 100644 index 0000000..92d8279 --- /dev/null +++ b/internal/modules/user/repository.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Braden Nicholson + +package user + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type userRepo struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) domain.UserRepository { + return &userRepo{ + db: db, + } +} + +func (u userRepo) FindAll() ([]*domain.User, error) { + var target []*domain.User + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u userRepo) FindById(id string) (*domain.User, error) { + var target *domain.User + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u userRepo) Create(user *domain.User) error { + if err := u.db.Create(user).Error; err != nil { + return err + } + return nil +} + +func (u userRepo) FindOrCreate(user *domain.User) error { + if err := u.db.FirstOrCreate(user).Error; err != nil { + return err + } + return nil +} + +func (u userRepo) Update(user *domain.User) error { + if err := u.db.Save(user).Error; err != nil { + return err + } + return nil +} + +func (u userRepo) Delete(user *domain.User) error { + if err := u.db.Delete(user).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/user/service.go b/internal/modules/user/service.go new file mode 100644 index 0000000..b77958e --- /dev/null +++ b/internal/modules/user/service.go @@ -0,0 +1,80 @@ +// Copyright (c) 2022 Braden Nicholson + +package user + +import ( + "fmt" + "golang.org/x/crypto/bcrypt" + "udap/internal/core/domain" +) + +type userService struct { + repository domain.UserRepository +} + +func NewUserService(repository domain.UserRepository) domain.UserService { + return userService{repository: repository} +} + +// Services + +func (u userService) Register(user *domain.User) error { + password, err := HashPassword(user.Password) + if err != nil { + return err + } + user.Password = password + err = u.repository.Create(user) + if err != nil { + return err + } + return nil +} + +func (u userService) Authenticate(user *domain.User) error { + ref, err := u.repository.FindById(user.Id) + if err != nil { + return err + } + hash := CheckPasswordHash(user.Password, ref.Password) + if !hash { + return fmt.Errorf("invalid password") + } + return nil +} + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// Repository Mapping + +func (u userService) FindAll() ([]*domain.User, error) { + return u.repository.FindAll() +} + +func (u userService) FindById(id string) (*domain.User, error) { + return u.repository.FindById(id) +} + +func (u userService) Create(user *domain.User) error { + return u.repository.Create(user) +} + +func (u userService) FindOrCreate(user *domain.User) error { + return u.repository.FindOrCreate(user) +} + +func (u userService) Update(user *domain.User) error { + return u.repository.Update(user) +} + +func (u userService) Delete(user *domain.User) error { + return u.repository.Delete(user) +} diff --git a/internal/modules/user/user.go b/internal/modules/user/user.go new file mode 100644 index 0000000..47c3e1f --- /dev/null +++ b/internal/modules/user/user.go @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Braden Nicholson + +package user + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func New(db *gorm.DB) domain.UserService { + repo := NewUserRepository(db) + service := NewUserService(repo) + return service +} diff --git a/internal/port/rest/user.go b/internal/port/rest/user.go new file mode 100644 index 0000000..cb76889 --- /dev/null +++ b/internal/port/rest/user.go @@ -0,0 +1,3 @@ +// Copyright (c) 2022 Braden Nicholson + +package rest diff --git a/internal/server/endpoints.go b/internal/server/endpoints.go index cd032d6..84c0984 100644 --- a/internal/server/endpoints.go +++ b/internal/server/endpoints.go @@ -12,6 +12,7 @@ import ( "net/http" "os" "sync" + "time" "udap/internal/bond" "udap/internal/controller" "udap/internal/log" @@ -151,11 +152,6 @@ func (e *Endpoints) socketAdaptor(w http.ResponseWriter, req *http.Request) { ep.Connection.Watch() }() - err = e.ctrl.EmitAll() - if err != nil { - return - } - go func() { defer wg.Done() var out []byte @@ -173,6 +169,14 @@ func (e *Endpoints) socketAdaptor(w http.ResponseWriter, req *http.Request) { } } }() + + go func() { + time.After(time.Millisecond * 500) + err = e.ctrl.EmitAll() + if err != nil { + return + } + }() wg.Wait() } diff --git a/internal/server/modules.go b/internal/server/modules.go index bdc46e4..b8ae1ce 100644 --- a/internal/server/modules.go +++ b/internal/server/modules.go @@ -29,27 +29,38 @@ const ( ) type ModuleController struct { - model models.Module + model models.Module + module plugin.ModuleInterface + + config plugin.Config + + state string loaded bool running bool - module plugin.ModuleInterface - config plugin.Config - source string - binary string - state string - c chan ModuleState - mid string + ctrl *controller.Controller + c chan ModuleState receive chan models.Module + + source string + binary string + mid string } func (m *ModuleController) listen() { for module := range m.receive { if m.model.Enabled != module.Enabled { - if !module.Enabled { - log.Event("Disabling module '%s'", m.model.Name) + if module.Enabled { + err := m.enable() + if err != nil { + return + } } else { - log.Event("Enabling module '%s'", m.model.Name) + err := m.disable() + if err != nil { + return + } + } } m.model = module @@ -85,6 +96,24 @@ func (m *ModuleController) setState(state string) { } } +func (m *ModuleController) enable() error { + if m.running { + log.Event("Module '%s' is already running.", m.model.Name) + return nil + } + log.Event("Enabling module '%s'", m.model.Name) + err := m.start() + if err != nil { + log.Err(err) + } + return nil +} + +func (m *ModuleController) disable() error { + log.Event("Disabling module '%s'", m.model.Name) + m.running = false + return nil +} func (m *ModuleController) build() error { // Create a timeout to prevent modules from taking too long to build timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*15) @@ -103,12 +132,22 @@ func (m *ModuleController) build() error { return nil } -func (m *ModuleController) setup(ctrl *controller.Controller, c chan ModuleState) error { +func (m *ModuleController) init(ctrl *controller.Controller, c chan ModuleState) error { + m.state = UNINITIALIZED + m.ctrl = ctrl + m.c = c + err := m.setup() + if err != nil { + return err + } + return nil +} + +func (m *ModuleController) setup() error { err := m.build() if err != nil { return err } - m.state = UNINITIALIZED p, err := plugin.Load(m.binary) if err != nil { return err @@ -120,13 +159,15 @@ func (m *ModuleController) setup(ctrl *controller.Controller, c chan ModuleState m.module = mod // Defer the wait group to complete at the end // Attempt to connect to the module - err = m.module.Connect(ctrl) + err = m.module.Connect(m.ctrl) if err != nil { return err } // Run module setup m.config, err = p.Setup() if err != nil { + m.setState(UNINITIALIZED) + m.running = false log.ErrF(err, "Module '%s' setup failed: ", m.config.Name) return err } @@ -141,8 +182,8 @@ func (m *ModuleController) setup(ctrl *controller.Controller, c chan ModuleState State: m.state, Channel: m.receive, } - m.c = c - mid, err := ctrl.Modules.Register(modDb) + + mid, err := m.ctrl.Modules.Register(modDb) if err != nil { return err } @@ -152,6 +193,7 @@ func (m *ModuleController) setup(ctrl *controller.Controller, c chan ModuleState m.setState(IDLE) m.loaded = true + return nil } @@ -162,7 +204,7 @@ func (m *ModuleController) start() error { // Attempt to run the module m.setState(RUNNING) go func() { - for { + for m.model.Enabled { err := m.module.Run() if err != nil { log.ErrF(err, "Module '%s' terminated prematurely: ", m.config.Name) @@ -271,16 +313,11 @@ func (m *Modules) Run() error { // Run a go function to create a new thread go func(p *ModuleController) { defer wg.Done() - err := p.setup(m.ctrl, m.state) + err := p.init(m.ctrl, m.state) if err != nil { log.ErrF(err, "Module '%s' setup failed: ", p.config.Name) return } - // Attempt to run the module - err = p.start() - if err != nil { - return - } }(module) } wg.Wait() diff --git a/modules/spotify/spotify.go b/modules/spotify/spotify.go index 636861e..b2294b2 100644 --- a/modules/spotify/spotify.go +++ b/modules/spotify/spotify.go @@ -73,6 +73,8 @@ func (s *Spotify) PutAttribute(key string) models.FuncPut { if err != nil { return err } + err = s.Attributes.Set(s.id, "playing", str) + break } return nil @@ -292,7 +294,7 @@ func (s *Spotify) push() error { } res := "false" if sp.Playing { - s.Frequency = 5000 + s.Frequency = 3000 res = "true" } else { s.Frequency = 15000 diff --git a/modules/squid/squid.go b/modules/squid/squid.go index bca387b..1241fdc 100644 --- a/modules/squid/squid.go +++ b/modules/squid/squid.go @@ -4,12 +4,15 @@ package main import ( "fmt" + "math" + "os" "strconv" "sync" + "time" "udap/internal/log" "udap/internal/models" - "udap/internal/pkg/dmx" - "udap/internal/pkg/dmx/ft232" + "udap/pkg/dmx" + "udap/pkg/dmx/ft232" "udap/pkg/plugin" ) @@ -20,8 +23,16 @@ type Squid struct { dmx ft232.DMXController state map[int]int entities map[int]string - stateMutex sync.RWMutex - connected bool + stateMutex sync.Mutex + update chan Command + + connected bool +} + +type Command struct { + read bool + id int + value int } func init() { @@ -29,14 +40,13 @@ func init() { Name: "squid", Type: "module", Description: "Control LOR Light Controller", - Version: "2.0.1", + Version: "1.2", Author: "Braden Nicholson", } Module.Config = config } -// setChannelValue sends a dmx signal to the provide channel with the provided value func (s *Squid) setChannelValue(channel int, value int) (err error) { if !s.connected { return fmt.Errorf("squid is not connected") @@ -45,10 +55,10 @@ func (s *Squid) setChannelValue(channel int, value int) (err error) { if value > 100 || value < 0 { return fmt.Errorf("desired value '%d' is invalid", value) } + var adjustedValue byte + adjustedValue = uint8(math.Round((float64(value) / 100.0) * 255.0)) - adjustedValue := (value / 100.0) * 255 - - err = s.dmx.SetChannel(int16(channel), byte(adjustedValue)) + err = s.dmx.SetChannel(int16(channel), adjustedValue) if err != nil { return err } @@ -56,19 +66,20 @@ func (s *Squid) setChannelValue(channel int, value int) (err error) { return nil } -// getChannelValue polls the dmx controller for the current value of the channel -func (s *Squid) getChannelValue(channel int) (value int, err error) { - if !s.connected { - return 0, fmt.Errorf("squid is not connected") - } - - res, err := s.dmx.GetChannel(int16(channel)) - if err != nil { - return 0, err - } - newValue := (res / 255.0) * 100.0 - return int(newValue), nil -} +// +// // getChannelValue polls the dmx controller for the current value of the channel +// func (s *Squid) getChannelValue(channel int) (value int, err error) { +// if !s.connected { +// return 0, fmt.Errorf("squid is not connected") +// } +// +// res, err := s.dmx.GetChannel(int16(channel)) +// if err != nil { +// return 0, err +// } +// newValue := +// return int(newValue), nil +// } func (s *Squid) isLocalOn(channel int) (value bool) { value = false @@ -90,37 +101,78 @@ func (s *Squid) getLocalValue(channel int) (value int) { func (s *Squid) setLocalValue(channel int, value int) error { - s.stateMutex.Lock() - s.state[channel] = value - s.stateMutex.Unlock() + s.update <- Command{ + read: false, + id: channel, + value: value, + } return nil } -// isChannelOn provides a boolean describing the on state of the channel -func (s *Squid) isChannelOn(channel int) (value bool, err error) { - if !s.connected { - return false, fmt.Errorf("squid is not connected") +func (s *Squid) remoteGetOn(id int) func() (string, error) { + return func() (string, error) { + state := "false" + if s.isLocalOn(id) { + state = "true" + } + return state, nil } - channelValue, err := s.getChannelValue(channel) - if err != nil { - return false, err +} + +func (s *Squid) remotePutOn(id int) func(value string) error { + return func(value string) error { + if value == "true" { + err := s.setLocalValue(id, 100) + if err != nil { + return err + } + } else { + err := s.setLocalValue(id, 0) + if err != nil { + return err + } + } + return nil } - if channelValue > 0 { - return true, nil +} + +func (s *Squid) remoteGetDim(id int) func() (string, error) { + return func() (string, error) { + state := "0" + value := s.getLocalValue(id) + state = fmt.Sprintf("%d", value) + + return state, nil + } +} + +func (s *Squid) remotePutDim(id int) func(value string) error { + return func(value string) error { + + parseInt, err := strconv.ParseInt(value, 10, 16) + if err != nil { + return err + } + + err = s.setLocalValue(id, int(parseInt)) + if err != nil { + return err + } + + return nil } - return false, nil } -func (s *Squid) findDevices() error { +func (s *Squid) registerDevices() error { if !s.connected { return nil } for i := 1; i <= 16; i++ { - name := fmt.Sprintf("ch%d", i) - entity := models.NewDimmer(name, s.Name) - res, err := s.Entities.Register(entity) + entity := models.NewDimmer(fmt.Sprintf("ch%d", i), s.Config.Name) + + res, err := s.Entities.Register(entity) if err != nil { return err } @@ -136,30 +188,8 @@ func (s *Squid) findDevices() error { s.entities[i] = res.Id - on.FnGet(func() (string, error) { - state := "off" - if s.isLocalOn(i) { - state = "on" - } - return state, nil - }) - - on.FnPut(func(value string) error { - - if value == "on" { - err = s.setLocalValue(i, 100) - if err != nil { - return err - } - } else { - err = s.setLocalValue(i, 0) - if err != nil { - return err - } - } - - return nil - }) + on.FnGet(s.remoteGetOn(i)) + on.FnPut(s.remotePutOn(i)) err = s.Attributes.Register(&on) if err != nil { @@ -175,31 +205,8 @@ func (s *Squid) findDevices() error { Entity: res.Id, } - dim.FnGet(func() (string, error) { - state := "0" - value := s.getLocalValue(i) - if err != nil { - return "", err - } - state = fmt.Sprintf("%d", value) - - return state, nil - }) - - dim.FnPut(func(value string) error { - - parseInt, err := strconv.ParseInt(value, 10, 16) - if err != nil { - return err - } - - err = s.setLocalValue(i, int(parseInt)) - if err != nil { - return err - } - - return nil - }) + dim.FnGet(s.remoteGetDim(i)) + dim.FnPut(s.remotePutDim(i)) err = s.Attributes.Register(&dim) if err != nil { @@ -213,14 +220,24 @@ func (s *Squid) findDevices() error { // Setup is called once at the launch of the module func (s *Squid) Setup() (plugin.Config, error) { - + err := s.UpdateInterval(2000) + if err != nil { + return plugin.Config{}, err + } return s.Config, nil } func (s *Squid) connect() error { s.connected = false + + so := os.Stdout + os.Stdout = os.DevNull + config := dmx.NewConfig(0x02) config.GetUSBContext() + + os.Stdout = so + defer func() { if r := recover(); r != nil { s.connected = false @@ -228,6 +245,7 @@ func (s *Squid) connect() error { return } }() + s.dmx = ft232.NewDMXController(config) err := s.dmx.Connect() @@ -235,31 +253,46 @@ func (s *Squid) connect() error { return err } - s.stateMutex = sync.RWMutex{} + s.connected = true + s.update = make(chan Command, 4) + + s.stateMutex = sync.Mutex{} s.stateMutex.Lock() s.state = map[int]int{} s.stateMutex.Unlock() + s.entities = map[int]string{} - s.connected = true - err = s.findDevices() + + go s.mux() + + err = s.registerDevices() if err != nil { return err } + return nil } +func (s *Squid) mux() { + for cmd := range s.update { + s.stateMutex.Lock() + s.state[cmd.id] = cmd.value + s.stateMutex.Unlock() + } +} + func (s *Squid) pull() error { if !s.connected { return nil } for i, entity := range s.entities { - state := "off" + state := "false" if s.isLocalOn(i) { - state = "on" + state = "true" } - err := s.Attributes.Set(entity, "on", state) + err := s.Attributes.Update(entity, "on", state, time.Now()) if err != nil { return err } @@ -267,7 +300,7 @@ func (s *Squid) pull() error { state = "0" value := s.getLocalValue(i) state = fmt.Sprintf("%d", value) - err = s.Attributes.Set(entity, "dim", state) + err = s.Attributes.Update(entity, "dim", state, time.Now()) if err != nil { return err } @@ -279,38 +312,51 @@ func (s *Squid) pull() error { // Update is called every cycle func (s *Squid) Update() error { - // pulse.Fixed(1000) - // defer pulse.End() - // if time.Since(s.Module.LastUpdate) >= time.Second*10 { - // s.Module.LastUpdate = time.Now() - // return s.pull() - // } + if s.Ready() { + err := s.pull() + if err != nil { + return err + } + } return nil } // Run is called after Setup, concurrent with Update func (s *Squid) Run() (err error) { - // - // err = s.connect() - // if err != nil { - // return err - // } - // - // for { - // s.stateMutex.RLock() - // for k, v := range s.state { - // err = s.dmx.SetChannel(int16(k), byte(v)) - // if err != nil { - // log.Err(err) - // } - // } - // s.stateMutex.RUnlock() - // - // err = s.dmx.Render() - // if err != nil { - // log.Err(err) - // } - // time.Sleep(20 * time.Millisecond) - // } + + err = s.connect() + if err != nil { + return err + } + + for { + + if !s.connected { + break + } + + s.stateMutex.Lock() + for k, v := range s.state { + err = s.setChannelValue(k, v) + if err != nil { + return err + } + } + s.stateMutex.Unlock() + + err = s.dmx.Render() + if err != nil { + log.Err(err) + break + } + time.Sleep(50 * time.Millisecond) + + } + + err = s.dmx.Close() + if err != nil { + return err + } + return nil } diff --git a/modules/weather/weather.go b/modules/weather/weather.go index 0e797eb..4ad7dda 100644 --- a/modules/weather/weather.go +++ b/modules/weather/weather.go @@ -138,7 +138,10 @@ func (v *Weather) fetchWeather() error { } func (v *Weather) Setup() (plugin.Config, error) { - + err := v.UpdateInterval(15000) + if err != nil { + return plugin.Config{}, err + } return v.Config, nil } @@ -158,10 +161,11 @@ func (v *Weather) pull() error { return nil } func (v *Weather) Update() error { - - if time.Since(v.Module.LastUpdate) >= time.Minute*15 { - v.Module.LastUpdate = time.Now() - return v.pull() + if v.Ready() { + err := v.pull() + if err != nil { + return err + } } return nil } diff --git a/modules/webstats/webstats.go b/modules/webstats/webstats.go new file mode 100644 index 0000000..a932bd3 --- /dev/null +++ b/modules/webstats/webstats.go @@ -0,0 +1,57 @@ +// Copyright (c) 2021 Braden Nicholson + +package main + +import ( + "time" + "udap/internal/log" + "udap/pkg/plugin" +) + +var Module WebStats + +type WebStats struct { + plugin.Module + eId string +} + +func init() { + config := plugin.Config{ + Name: "webstats", + Type: "module", + Description: "Web related statistics", + Version: "0.0.1", + Author: "Braden Nicholson", + } + Module.Config = config +} + +func (w *WebStats) Setup() (plugin.Config, error) { + err := w.UpdateInterval(2000) + if err != nil { + return plugin.Config{}, err + } + return w.Config, nil +} + +func (w *WebStats) pull() error { + time.Sleep(250 * time.Millisecond) + return nil +} + +func (w *WebStats) Update() error { + if w.Ready() { + err := w.pull() + if err != nil { + return err + } + } + return nil +} + +func (w *WebStats) Run() error { + log.Log("Webstats running") + time.Sleep(time.Second * 2) + log.Log("Webstats exiting") + return nil +} From c6d3948ef98fc9c1aad4aa0a76d436aec5f917e3 Mon Sep 17 00:00:00 2001 From: Braden Date: Fri, 27 May 2022 17:03:49 -0700 Subject: [PATCH 02/18] Migrated user and modules to hexagon structure, basic integration with controller. Rest endpoints created for modules. --- internal/controller/controller.go | 36 ++++++------- internal/core/domain/module.go | 11 +++- internal/modules/module/module.go | 10 ++++ internal/modules/module/repository.go | 55 ++++--------------- internal/modules/module/service.go | 29 +++++++--- internal/modules/user/repository.go | 2 +- internal/modules/user/service.go | 2 +- internal/modules/user/user.go | 4 +- internal/port/rest/module.go | 78 +++++++++++++++++++++++++++ pkg/plugin/default.go | 12 +++++ 10 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 internal/port/rest/module.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index f102b93..001c33e 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -5,31 +5,39 @@ package controller import ( "fmt" "udap/internal/bond" + "udap/internal/core/domain" + "udap/internal/modules/module" + "udap/internal/modules/user" "udap/internal/pulse" + "udap/internal/store" ) type Controller struct { - Entities *Entities - Attributes *Attributes - Modules *Modules - Endpoints *Endpoints - Devices *Devices - Zones *Zones - Networks *Networks - Users *Users - event chan bond.Msg + Entities *Entities + Attributes *Attributes + Modules *Modules + Endpoints *Endpoints + Devices *Devices + Zones *Zones + Networks *Networks + ModuleService domain.ModuleService + Users domain.UserService + event chan bond.Msg } func NewController() (*Controller, error) { c := &Controller{} + c.ModuleService = module.New() + + c.Users = user.New(store.DB.DB) + c.Entities = LoadEntities() c.Modules = LoadModules() c.Attributes = LoadAttributes() c.Endpoints = LoadEndpoints() c.Devices = LoadDevices() c.Networks = LoadNetworks() - c.Users = LoadUsers() c.Zones = LoadZones() c.Modules = LoadModules() return c, nil @@ -40,8 +48,6 @@ func (c *Controller) Handle(msg bond.Msg) (interface{}, error) { pulse.LogGlobal("-> Ctrl::%s %s", msg.Target, msg.Operation) switch t := msg.Target; t { - case "user": - return c.Users.Handle(msg) case "entity": return c.Entities.Handle(msg) case "attribute": @@ -98,12 +104,6 @@ func (c *Controller) EmitAll() error { if err != nil { return err } - - err = c.Users.EmitAll() - if err != nil { - return err - } - err = c.Zones.EmitAll() if err != nil { return err diff --git a/internal/core/domain/module.go b/internal/core/domain/module.go index cb17364..c2db21c 100644 --- a/internal/core/domain/module.go +++ b/internal/core/domain/module.go @@ -2,6 +2,8 @@ package domain +import "udap/pkg/plugin" + type Module struct { Persistent Name string `json:"name"` @@ -12,16 +14,21 @@ type Module struct { Author string `json:"author"` Channel chan Module `json:"-" gorm:"-"` State string `json:"state"` - Enabled bool `json:"enabled" gorm:"default:true"` - Recover int `json:"recover"` + plugin.ModuleInterface + Enabled bool `json:"enabled" gorm:"default:true"` + Recover int `json:"recover"` } type ModuleRepository interface { + Candidates() ([]string, error) FindAll() ([]*Module, error) FindByName(name string) (*Module, error) } type ModuleService interface { + Discover() error + Build(name string) error + BuildAll() error FindAll() ([]*Module, error) FindByName(name string) (*Module, error) Disable(name string) error diff --git a/internal/modules/module/module.go b/internal/modules/module/module.go index e25e588..527cf27 100644 --- a/internal/modules/module/module.go +++ b/internal/modules/module/module.go @@ -1,3 +1,13 @@ // Copyright (c) 2022 Braden Nicholson package module + +import ( + "udap/internal/core/domain" +) + +func New() domain.ModuleService { + repo := NewRepository() + service := NewService(repo) + return service +} diff --git a/internal/modules/module/repository.go b/internal/modules/module/repository.go index d3a85b5..e8786a8 100644 --- a/internal/modules/module/repository.go +++ b/internal/modules/module/repository.go @@ -3,60 +3,27 @@ package module import ( - "gorm.io/gorm" "udap/internal/core/domain" ) type moduleRepo struct { - db *gorm.DB } -func NewUserRepository(db *gorm.DB) domain.UserRepository { - return &moduleRepo{ - db: db, - } +func NewRepository() domain.ModuleRepository { + return &moduleRepo{} } -func (u moduleRepo) FindAll() ([]*domain.User, error) { - var target []*domain.User - if err := u.db.First(target).Error; err != nil { - return nil, err - } - return target, nil +func (u moduleRepo) Candidates() ([]string, error) { + // TODO implement me + panic("implement me") } -func (u moduleRepo) FindById(id string) (*domain.User, error) { - var target *domain.User - if err := u.db.Where("id = ?", id).First(target).Error; err != nil { - return nil, err - } - return target, nil +func (u moduleRepo) FindAll() ([]*domain.Module, error) { + // TODO implement me + panic("implement me") } -func (u moduleRepo) Create(user *domain.User) error { - if err := u.db.Create(user).Error; err != nil { - return err - } - return nil -} - -func (u moduleRepo) FindOrCreate(user *domain.User) error { - if err := u.db.FirstOrCreate(user).Error; err != nil { - return err - } - return nil -} - -func (u moduleRepo) Update(user *domain.User) error { - if err := u.db.Save(user).Error; err != nil { - return err - } - return nil -} - -func (u moduleRepo) Delete(user *domain.User) error { - if err := u.db.Delete(user).Error; err != nil { - return err - } - return nil +func (u moduleRepo) FindByName(name string) (*domain.Module, error) { + // TODO implement me + panic("implement me") } diff --git a/internal/modules/module/service.go b/internal/modules/module/service.go index 052364e..0e5c978 100644 --- a/internal/modules/module/service.go +++ b/internal/modules/module/service.go @@ -10,7 +10,22 @@ type moduleService struct { repository domain.ModuleRepository } -func NewModuleService(repository domain.ModuleRepository) domain.ModuleService { +func (u moduleService) Discover() error { + // TODO implement me + panic("implement me") +} + +func (u moduleService) Build(name string) error { + // TODO implement me + panic("implement me") +} + +func (u moduleService) BuildAll() error { + // TODO implement me + panic("implement me") +} + +func NewService(repository domain.ModuleRepository) domain.ModuleService { return moduleService{repository: repository} } @@ -24,14 +39,12 @@ func (u moduleService) FindByName(name string) (*domain.Module, error) { return u.repository.FindByName(name) } -func (u moduleService) FindById(id string) (*domain.Module, error) { - // TODO implement me - panic("implement me") -} - func (u moduleService) Disable(name string) error { - // TODO implement me - panic("implement me") + _, err := u.FindByName(name) + if err != nil { + return err + } + return nil } func (u moduleService) Enable(name string) error { diff --git a/internal/modules/user/repository.go b/internal/modules/user/repository.go index 92d8279..e26cf08 100644 --- a/internal/modules/user/repository.go +++ b/internal/modules/user/repository.go @@ -11,7 +11,7 @@ type userRepo struct { db *gorm.DB } -func NewUserRepository(db *gorm.DB) domain.UserRepository { +func NewRepository(db *gorm.DB) domain.UserRepository { return &userRepo{ db: db, } diff --git a/internal/modules/user/service.go b/internal/modules/user/service.go index b77958e..3080f67 100644 --- a/internal/modules/user/service.go +++ b/internal/modules/user/service.go @@ -12,7 +12,7 @@ type userService struct { repository domain.UserRepository } -func NewUserService(repository domain.UserRepository) domain.UserService { +func NewService(repository domain.UserRepository) domain.UserService { return userService{repository: repository} } diff --git a/internal/modules/user/user.go b/internal/modules/user/user.go index 47c3e1f..96f71fd 100644 --- a/internal/modules/user/user.go +++ b/internal/modules/user/user.go @@ -8,7 +8,7 @@ import ( ) func New(db *gorm.DB) domain.UserService { - repo := NewUserRepository(db) - service := NewUserService(repo) + repo := NewRepository(db) + service := NewService(repo) return service } diff --git a/internal/port/rest/module.go b/internal/port/rest/module.go new file mode 100644 index 0000000..b3cf126 --- /dev/null +++ b/internal/port/rest/module.go @@ -0,0 +1,78 @@ +// Copyright (c) 2022 Braden Nicholson + +package rest + +import ( + "github.com/go-chi/chi" + "net/http" + "udap/internal/core/domain" +) + +type ModuleRouter interface { + RouteModules(router chi.Router) +} + +type moduleRouter struct { + service domain.ModuleService +} + +func NewModuleRouter(service domain.ModuleService) ModuleRouter { + return moduleRouter{ + service: service, + } +} + +func (r moduleRouter) RouteModules(router chi.Router) { + router.Route("/modules", func(local chi.Router) { + local.Route("/{name}", func(named chi.Router) { + named.Post("/build", r.build) + named.Post("/disable", r.disable) + named.Post("/enable", r.enable) + named.Post("/halt", r.halt) + }) + }) +} + +func (r moduleRouter) build(w http.ResponseWriter, req *http.Request) { + name := chi.URLParam(req, "name") + if name != "" { + err := r.service.Build(name) + if err != nil { + http.Error(w, "invalid module name", 401) + } + } + w.WriteHeader(200) +} + +func (r moduleRouter) enable(w http.ResponseWriter, req *http.Request) { + name := chi.URLParam(req, "name") + if name != "" { + err := r.service.Enable(name) + if err != nil { + http.Error(w, "invalid module name", 401) + } + } + w.WriteHeader(200) +} + +func (r moduleRouter) disable(w http.ResponseWriter, req *http.Request) { + name := chi.URLParam(req, "name") + if name != "" { + err := r.service.Disable(name) + if err != nil { + http.Error(w, "invalid module name", 401) + } + } + w.WriteHeader(200) +} + +func (r moduleRouter) halt(w http.ResponseWriter, req *http.Request) { + name := chi.URLParam(req, "name") + if name != "" { + err := r.service.Halt(name) + if err != nil { + http.Error(w, "invalid module name", 401) + } + } + w.WriteHeader(200) +} diff --git a/pkg/plugin/default.go b/pkg/plugin/default.go index 326a3a3..1ed718c 100644 --- a/pkg/plugin/default.go +++ b/pkg/plugin/default.go @@ -22,6 +22,18 @@ type Module struct { *controller.Controller } +// UpdateInterval is called once at the launch of the module +func (m *Module) UpdateInterval(frequency int) error { + m.LastUpdate = time.Now() + m.Frequency = frequency + return nil +} + +// Ready is called once at the launch of the module +func (m *Module) Ready() bool { + return time.Since(m.LastUpdate) > time.Duration(m.Frequency)*time.Millisecond +} + // Connect is called once at the launch of the module func (m *Module) Connect(ctrl *controller.Controller) error { m.LastUpdate = time.Now() From 6b7c5e5aab08515e700974671bc4818948031b32 Mon Sep 17 00:00:00 2001 From: Braden Date: Sat, 28 May 2022 14:58:19 -0700 Subject: [PATCH 03/18] Migrated entity, attribute, and other domains into the new architecture. Added routing for the appropriate components. Re-implemented modules. Reconfigured modules to work with the new models. --- cmd/udap/main.go | 57 ++++++++ internal/controller/controller.go | 122 +++++++----------- internal/core/domain/attribute.go | 43 ++++++ internal/core/domain/device.go | 9 ++ internal/core/domain/endpoint.go | 35 +++++ internal/core/domain/entity.go | 38 ++++++ internal/core/domain/module.go | 19 +-- internal/modules/attribute/attribute.go | 14 ++ internal/modules/attribute/repository.go | 78 +++++++++++ internal/modules/attribute/service.go | 73 +++++++++++ internal/modules/endpoint/endpoint.go | 14 ++ internal/modules/endpoint/repository.go | 70 ++++++++++ internal/modules/endpoint/service.go | 55 ++++++++ internal/modules/entity/entity.go | 14 ++ internal/modules/entity/repository.go | 62 +++++++++ internal/modules/entity/service.go | 59 +++++++++ internal/modules/module/module.go | 5 +- internal/modules/module/repository.go | 63 +++++++-- internal/modules/module/service.go | 90 +++++++++++-- internal/orchestrator/orchestrator.go | 71 ++++++++++ internal/port/routes/endpoint.go | 68 ++++++++++ internal/port/{rest => routes}/module.go | 8 +- internal/port/routes/user.go | 54 ++++++++ internal/port/runtimes/module.go | 22 ++++ .../{port/rest/user.go => udap/runner.go} | 6 +- modules/homekit/homekit.go | 51 ++++---- modules/hs100/hs100.go | 24 +++- modules/macmeta/macmeta.go | 45 ++++--- modules/spotify/spotify.go | 48 +++++-- modules/weather/weather.go | 18 +-- platform/database/database.go | 36 ++++++ platform/jwt/auth.go | 47 +++++++ platform/router/auth.go | 47 +++++++ platform/router/router.go | 39 ++++++ 34 files changed, 1317 insertions(+), 187 deletions(-) create mode 100644 cmd/udap/main.go create mode 100644 internal/core/domain/attribute.go create mode 100644 internal/core/domain/endpoint.go create mode 100644 internal/core/domain/entity.go create mode 100644 internal/modules/attribute/attribute.go create mode 100644 internal/modules/attribute/repository.go create mode 100644 internal/modules/attribute/service.go create mode 100644 internal/modules/endpoint/endpoint.go create mode 100644 internal/modules/endpoint/repository.go create mode 100644 internal/modules/endpoint/service.go create mode 100644 internal/modules/entity/entity.go create mode 100644 internal/modules/entity/repository.go create mode 100644 internal/modules/entity/service.go create mode 100644 internal/orchestrator/orchestrator.go create mode 100644 internal/port/routes/endpoint.go rename internal/port/{rest => routes}/module.go (93%) create mode 100644 internal/port/routes/user.go create mode 100644 internal/port/runtimes/module.go rename internal/{port/rest/user.go => udap/runner.go} (55%) create mode 100644 platform/database/database.go create mode 100644 platform/jwt/auth.go create mode 100644 platform/router/auth.go create mode 100644 platform/router/router.go diff --git a/cmd/udap/main.go b/cmd/udap/main.go new file mode 100644 index 0000000..030005f --- /dev/null +++ b/cmd/udap/main.go @@ -0,0 +1,57 @@ +// Copyright (c) 2022 Braden Nicholson + +package main + +import ( + "fmt" + "github.com/joho/godotenv" + "os" + "udap/internal/log" + "udap/internal/orchestrator" +) + +const VERSION = "2.13" + +func main() { + + err := setup() + if err != nil { + return + } + + // Initialize Orchestrator + o := orchestrator.NewOrchestrator() + + // Initialize services + err = o.Init() + if err != nil { + return + } + + // Run udap + err = o.Run() + if err != nil { + return + } +} + +func setup() error { + log.Log("UDAP v%s - Copyright (c) 2019-2022 Braden Nicholson", VERSION) + + err := godotenv.Load() + if err != nil { + return fmt.Errorf("failed to load .env file") + } + + if os.Getenv("environment") == "production" { + log.Log("Running in PRODUCTION mode.") + } else { + log.Log("Running in DEVELOPMENT mode.") + } + + err = os.Setenv("version", VERSION) + if err != nil { + return err + } + return nil +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 001c33e..b01e3f5 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -4,42 +4,39 @@ package controller import ( "fmt" + "gorm.io/gorm" "udap/internal/bond" "udap/internal/core/domain" + "udap/internal/modules/attribute" + "udap/internal/modules/endpoint" + "udap/internal/modules/entity" "udap/internal/modules/module" "udap/internal/modules/user" "udap/internal/pulse" - "udap/internal/store" ) type Controller struct { - Entities *Entities - Attributes *Attributes - Modules *Modules - Endpoints *Endpoints Devices *Devices Zones *Zones Networks *Networks + Attributes domain.AttributeService + Modules domain.ModuleService + Entities domain.EntityService + Endpoints domain.EndpointService ModuleService domain.ModuleService Users domain.UserService event chan bond.Msg } -func NewController() (*Controller, error) { +func NewController(db *gorm.DB) (*Controller, error) { c := &Controller{} - c.ModuleService = module.New() + c.Users = user.New(db) + c.Endpoints = endpoint.New(db) + c.Attributes = attribute.New(db) + c.Entities = entity.New(db) + c.Modules = module.New(db) - c.Users = user.New(store.DB.DB) - - c.Entities = LoadEntities() - c.Modules = LoadModules() - c.Attributes = LoadAttributes() - c.Endpoints = LoadEndpoints() - c.Devices = LoadDevices() - c.Networks = LoadNetworks() - c.Zones = LoadZones() - c.Modules = LoadModules() return c, nil } @@ -47,67 +44,42 @@ func (c *Controller) Handle(msg bond.Msg) (interface{}, error) { pulse.LogGlobal("-> Ctrl::%s %s", msg.Target, msg.Operation) - switch t := msg.Target; t { - case "entity": - return c.Entities.Handle(msg) - case "attribute": - return c.Attributes.Handle(msg) - case "module": - return c.Modules.Handle(msg) - case "endpoint": - return c.Endpoints.Handle(msg) - case "device": - return c.Devices.Handle(msg) - case "network": - return c.Networks.Handle(msg) - case "zone": - return c.Zones.Handle(msg) - default: - return nil, fmt.Errorf("unknown target '%s'", t) - } + // switch t := msg.Target; t { + // case "attribute": + // return c.Attributes.Handle(msg) + // case "device": + // return c.Devices.Handle(msg) + // case "network": + // return c.Networks.Handle(msg) + // case "zone": + // return c.Zones.Handle(msg) + // default: + // return nil, fmt.Errorf("unknown target '%s'", t) + // } + return nil, nil } func (c *Controller) EmitAll() error { - var err error - - err = c.Entities.EmitAll() - if err != nil { - return err - } - - err = c.Attributes.EmitAll() - if err != nil { - return err - } - - err = c.Networks.EmitAll() - if err != nil { - return err - } - - err = c.Devices.EmitAll() - if err != nil { - return err - } - - err = c.Modules.EmitAll() - if err != nil { - return err - } - - err = c.Endpoints.EmitAll() - if err != nil { - return err - } - - err = c.Endpoints.EmitAll() - if err != nil { - return err - } - err = c.Zones.EmitAll() - if err != nil { - return err - } + // var err error + // err = c.Attributes.EmitAll() + // if err != nil { + // return err + // } + // + // err = c.Networks.EmitAll() + // if err != nil { + // return err + // } + // + // err = c.Devices.EmitAll() + // if err != nil { + // return err + // } + // + // err = c.Zones.EmitAll() + // if err != nil { + // return err + // } return nil } diff --git a/internal/core/domain/attribute.go b/internal/core/domain/attribute.go new file mode 100644 index 0000000..2d67ec3 --- /dev/null +++ b/internal/core/domain/attribute.go @@ -0,0 +1,43 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +import "time" + +type Attribute struct { + Persistent + Value string `json:"value"` + Updated time.Time `json:"updated"` + Request string `json:"request"` + Requested time.Time `json:"requested"` + Entity string `json:"entity"` + Key string `json:"key"` + Type string `json:"type"` + Order int `json:"order"` + Channel chan Attribute + // put FuncPut + // get FuncGet +} + +type AttributeRepository interface { + FindAll() (*[]Attribute, error) + FindAllByEntity(entity string) (*[]Attribute, error) + FindById(id string) (*Attribute, error) + FindByComposite(entity string, key string) (*Attribute, error) + Create(*Attribute) error + FindOrCreate(*Attribute) error + Update(*Attribute) error + Delete(*Attribute) error +} + +type AttributeService interface { + FindAll() (*[]Attribute, error) + FindAllByEntity(entity string) (*[]Attribute, error) + FindById(id string) (*Attribute, error) + Create(*Attribute) error + Register(*Attribute) error + Request(entity string, key string, value string) error + Set(entity string, key string, value string) error + Update(entity string, key string, value string, stamp time.Time) error + Delete(*Attribute) error +} diff --git a/internal/core/domain/device.go b/internal/core/domain/device.go index e7ef1e3..96ff0bb 100644 --- a/internal/core/domain/device.go +++ b/internal/core/domain/device.go @@ -21,3 +21,12 @@ type DeviceRepository interface { Update(*Device) error Delete(*Device) error } + +type DeviceService interface { + FindAll() ([]*Device, error) + FindById(id string) (*Device, error) + Create(*Device) error + FindOrCreate(*Device) error + Update(*Device) error + Delete(*Device) error +} diff --git a/internal/core/domain/endpoint.go b/internal/core/domain/endpoint.go new file mode 100644 index 0000000..f8c5c36 --- /dev/null +++ b/internal/core/domain/endpoint.go @@ -0,0 +1,35 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type Endpoint struct { + Persistent + Name string `json:"name" gorm:"unique"` + Type string `json:"type" gorm:"default:'terminal'"` + Connected bool `json:"connected"` + Key string `json:"key"` +} + +type EndpointRepository interface { + FindAll() ([]*Endpoint, error) + FindById(id string) (*Endpoint, error) + FindByKey(key string) (*Endpoint, error) + Create(*Endpoint) error + FindOrCreate(*Endpoint) error + Update(*Endpoint) error + Delete(*Endpoint) error +} + +type EndpointService interface { + FindAll() ([]*Endpoint, error) + FindById(id string) (*Endpoint, error) + FindByKey(key string) (*Endpoint, error) + Create(*Endpoint) error + + Enroll(key string) (*Endpoint, error) + Disconnect(key string) error + + FindOrCreate(*Endpoint) error + Update(*Endpoint) error + Delete(*Endpoint) error +} diff --git a/internal/core/domain/entity.go b/internal/core/domain/entity.go new file mode 100644 index 0000000..d28a20d --- /dev/null +++ b/internal/core/domain/entity.go @@ -0,0 +1,38 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type Entity struct { + Persistent + Name string `gorm:"unique" json:"name"` // Given name from module + Alias string `json:"alias"` // Name from users + Type string `json:"type"` // Type of entity {Light, Sensor, Etc} + Module string `json:"module"` // Parent Module name + Locked bool `json:"locked"` // Is the Entity state locked? + Config string `json:"config"` + Position string `json:"position" gorm:"default:'{}'"` + Icon string `json:"icon" gorm:"default:'􀛮'"` // The icon to represent this entity + Frequency int `json:"frequency" gorm:"default:3000"` + Neural string `json:"neural" gorm:"default:'inactive'"` // Parent Module name + Predicted string `gorm:"-" json:"predicted"` // scalar +} + +type EntityRepository interface { + FindAll() (*[]Entity, error) + FindById(id string) (*Entity, error) + Create(*Entity) error + FindOrCreate(*Entity) error + Update(*Entity) error + Delete(*Entity) error +} + +type EntityService interface { + FindAll() (*[]Entity, error) + FindById(id string) (*Entity, error) + Create(*Entity) error + Config(id string, value string) error + FindOrCreate(*Entity) error + Register(*Entity) (*Entity, error) + Update(*Entity) error + Delete(*Entity) error +} diff --git a/internal/core/domain/module.go b/internal/core/domain/module.go index c2db21c..fb5a13e 100644 --- a/internal/core/domain/module.go +++ b/internal/core/domain/module.go @@ -2,8 +2,6 @@ package domain -import "udap/pkg/plugin" - type Module struct { Persistent Name string `json:"name"` @@ -14,22 +12,25 @@ type Module struct { Author string `json:"author"` Channel chan Module `json:"-" gorm:"-"` State string `json:"state"` - plugin.ModuleInterface - Enabled bool `json:"enabled" gorm:"default:true"` - Recover int `json:"recover"` + Enabled bool `json:"enabled" gorm:"default:true"` + Recover int `json:"recover"` } type ModuleRepository interface { - Candidates() ([]string, error) - FindAll() ([]*Module, error) + FindAll() (*[]Module, error) FindByName(name string) (*Module, error) + FindById(id string) (*Module, error) + Create(*Module) error + FindOrCreate(*Module) error + Update(*Module) error + Delete(*Module) error } type ModuleService interface { Discover() error - Build(name string) error + Build(module *Module) error BuildAll() error - FindAll() ([]*Module, error) + FindAll() (*[]Module, error) FindByName(name string) (*Module, error) Disable(name string) error Enable(name string) error diff --git a/internal/modules/attribute/attribute.go b/internal/modules/attribute/attribute.go new file mode 100644 index 0000000..fb4f783 --- /dev/null +++ b/internal/modules/attribute/attribute.go @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Braden Nicholson + +package attribute + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func New(db *gorm.DB) domain.AttributeService { + repo := NewRepository(db) + service := NewService(repo) + return service +} diff --git a/internal/modules/attribute/repository.go b/internal/modules/attribute/repository.go new file mode 100644 index 0000000..020450a --- /dev/null +++ b/internal/modules/attribute/repository.go @@ -0,0 +1,78 @@ +// Copyright (c) 2022 Braden Nicholson + +package attribute + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type attributeRepo struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) domain.AttributeRepository { + return &attributeRepo{ + db: db, + } +} + +func (u attributeRepo) FindByComposite(entity string, key string) (*domain.Attribute, error) { + var target *domain.Attribute + if err := u.db.Where("entity = ? && key = ?", entity, key).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u attributeRepo) FindAllByEntity(entity string) (*[]domain.Attribute, error) { + var target *[]domain.Attribute + if err := u.db.Where("entity = ?", entity).Find(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u attributeRepo) FindAll() (*[]domain.Attribute, error) { + var target *[]domain.Attribute + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u attributeRepo) FindById(id string) (*domain.Attribute, error) { + var target *domain.Attribute + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u attributeRepo) Create(attribute *domain.Attribute) error { + if err := u.db.Create(attribute).Error; err != nil { + return err + } + return nil +} + +func (u attributeRepo) FindOrCreate(attribute *domain.Attribute) error { + if err := u.db.FirstOrCreate(attribute).Error; err != nil { + return err + } + return nil +} + +func (u attributeRepo) Update(attribute *domain.Attribute) error { + if err := u.db.Save(attribute).Error; err != nil { + return err + } + return nil +} + +func (u attributeRepo) Delete(attribute *domain.Attribute) error { + if err := u.db.Delete(attribute).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/attribute/service.go b/internal/modules/attribute/service.go new file mode 100644 index 0000000..4d737c6 --- /dev/null +++ b/internal/modules/attribute/service.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Braden Nicholson + +package attribute + +import ( + "fmt" + "time" + "udap/internal/core/domain" +) + +type attributeService struct { + repository domain.AttributeRepository + hooks map[string]chan domain.Attribute +} + +func (u attributeService) FindAllByEntity(entity string) (*[]domain.Attribute, error) { + return u.repository.FindAllByEntity(entity) +} + +func NewService(repository domain.AttributeRepository) domain.AttributeService { + return attributeService{ + repository: repository, + hooks: map[string]chan domain.Attribute{}, + } +} + +func (u attributeService) Register(attribute *domain.Attribute) error { + attribute, err := u.repository.FindByComposite(attribute.Entity, attribute.Key) + if err != nil { + return err + } + if u.hooks[attribute.Id] != nil { + return fmt.Errorf("attribute already registered") + } + u.hooks[attribute.Id] = attribute.Channel + attribute.Channel = nil + return nil +} + +func (u attributeService) Request(entity string, key string, value string) error { + return nil +} + +func (u attributeService) Set(entity string, key string, value string) error { + return nil +} + +func (u attributeService) Update(entity string, key string, value string, stamp time.Time) error { + // TODO implement me + panic("implement me") +} + +// Repository Mapping + +func (u attributeService) FindAll() (*[]domain.Attribute, error) { + return u.repository.FindAll() +} + +func (u attributeService) FindById(id string) (*domain.Attribute, error) { + return u.repository.FindById(id) +} + +func (u attributeService) Create(attribute *domain.Attribute) error { + return u.repository.Create(attribute) +} + +func (u attributeService) FindOrCreate(attribute *domain.Attribute) error { + return u.repository.FindOrCreate(attribute) +} + +func (u attributeService) Delete(attribute *domain.Attribute) error { + return u.repository.Delete(attribute) +} diff --git a/internal/modules/endpoint/endpoint.go b/internal/modules/endpoint/endpoint.go new file mode 100644 index 0000000..881c927 --- /dev/null +++ b/internal/modules/endpoint/endpoint.go @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Braden Nicholson + +package endpoint + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func New(db *gorm.DB) domain.EndpointService { + repo := NewRepository(db) + service := NewService(repo) + return service +} diff --git a/internal/modules/endpoint/repository.go b/internal/modules/endpoint/repository.go new file mode 100644 index 0000000..42cdc73 --- /dev/null +++ b/internal/modules/endpoint/repository.go @@ -0,0 +1,70 @@ +// Copyright (c) 2022 Braden Nicholson + +package endpoint + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type endpointRepo struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) domain.EndpointRepository { + return &endpointRepo{ + db: db, + } +} + +func (u endpointRepo) FindByKey(key string) (*domain.Endpoint, error) { + var target *domain.Endpoint + if err := u.db.Where("key = ?", key).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u endpointRepo) FindAll() ([]*domain.Endpoint, error) { + var target []*domain.Endpoint + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u endpointRepo) FindById(id string) (*domain.Endpoint, error) { + var target *domain.Endpoint + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u endpointRepo) Create(endpoint *domain.Endpoint) error { + if err := u.db.Create(endpoint).Error; err != nil { + return err + } + return nil +} + +func (u endpointRepo) FindOrCreate(endpoint *domain.Endpoint) error { + if err := u.db.FirstOrCreate(endpoint).Error; err != nil { + return err + } + return nil +} + +func (u endpointRepo) Update(endpoint *domain.Endpoint) error { + if err := u.db.Save(endpoint).Error; err != nil { + return err + } + return nil +} + +func (u endpointRepo) Delete(endpoint *domain.Endpoint) error { + if err := u.db.Delete(endpoint).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/endpoint/service.go b/internal/modules/endpoint/service.go new file mode 100644 index 0000000..cdf666a --- /dev/null +++ b/internal/modules/endpoint/service.go @@ -0,0 +1,55 @@ +// Copyright (c) 2022 Braden Nicholson + +package endpoint + +import ( + "udap/internal/core/domain" +) + +type endpointService struct { + repository domain.EndpointRepository +} + +func (u endpointService) FindByKey(key string) (*domain.Endpoint, error) { + return u.repository.FindByKey(key) +} + +func (u endpointService) Enroll(key string) (*domain.Endpoint, error) { + // TODO implement me + panic("implement me") +} + +func (u endpointService) Disconnect(key string) error { + // TODO implement me + panic("implement me") +} + +func NewService(repository domain.EndpointRepository) domain.EndpointService { + return endpointService{repository: repository} +} + +// Repository Mapping + +func (u endpointService) FindAll() ([]*domain.Endpoint, error) { + return u.repository.FindAll() +} + +func (u endpointService) FindById(id string) (*domain.Endpoint, error) { + return u.repository.FindById(id) +} + +func (u endpointService) Create(endpoint *domain.Endpoint) error { + return u.repository.Create(endpoint) +} + +func (u endpointService) FindOrCreate(endpoint *domain.Endpoint) error { + return u.repository.FindOrCreate(endpoint) +} + +func (u endpointService) Update(endpoint *domain.Endpoint) error { + return u.repository.Update(endpoint) +} + +func (u endpointService) Delete(endpoint *domain.Endpoint) error { + return u.repository.Delete(endpoint) +} diff --git a/internal/modules/entity/entity.go b/internal/modules/entity/entity.go new file mode 100644 index 0000000..0c309d0 --- /dev/null +++ b/internal/modules/entity/entity.go @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Braden Nicholson + +package entity + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func New(db *gorm.DB) domain.EntityService { + repo := NewRepository(db) + service := NewService(repo) + return service +} diff --git a/internal/modules/entity/repository.go b/internal/modules/entity/repository.go new file mode 100644 index 0000000..070c86d --- /dev/null +++ b/internal/modules/entity/repository.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Braden Nicholson + +package entity + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type entityRepo struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) domain.EntityRepository { + return &entityRepo{ + db: db, + } +} + +func (u entityRepo) FindAll() (*[]domain.Entity, error) { + var target *[]domain.Entity + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u entityRepo) FindById(id string) (*domain.Entity, error) { + var target *domain.Entity + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u entityRepo) Create(entity *domain.Entity) error { + if err := u.db.Create(entity).Error; err != nil { + return err + } + return nil +} + +func (u entityRepo) FindOrCreate(entity *domain.Entity) error { + if err := u.db.FirstOrCreate(entity).Error; err != nil { + return err + } + return nil +} + +func (u entityRepo) Update(entity *domain.Entity) error { + if err := u.db.Save(entity).Error; err != nil { + return err + } + return nil +} + +func (u entityRepo) Delete(entity *domain.Entity) error { + if err := u.db.Delete(entity).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/entity/service.go b/internal/modules/entity/service.go new file mode 100644 index 0000000..890db61 --- /dev/null +++ b/internal/modules/entity/service.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Braden Nicholson + +package entity + +import ( + "udap/internal/core/domain" +) + +type entityService struct { + repository domain.EntityRepository +} + +func (u entityService) Config(id string, value string) error { + entity, err := u.FindById(id) + if err != nil { + return err + } + entity.Config = value + err = u.Update(entity) + if err != nil { + return err + } + return nil +} + +func (u entityService) Register(entity *domain.Entity) (*domain.Entity, error) { + // TODO implement me + panic("implement me") +} + +func NewService(repository domain.EntityRepository) domain.EntityService { + return entityService{repository: repository} +} + +// Repository Mapping + +func (u entityService) FindAll() (*[]domain.Entity, error) { + return u.repository.FindAll() +} + +func (u entityService) FindById(id string) (*domain.Entity, error) { + return u.repository.FindById(id) +} + +func (u entityService) Create(entity *domain.Entity) error { + return u.repository.Create(entity) +} + +func (u entityService) FindOrCreate(entity *domain.Entity) error { + return u.repository.FindOrCreate(entity) +} + +func (u entityService) Update(entity *domain.Entity) error { + return u.repository.Update(entity) +} + +func (u entityService) Delete(entity *domain.Entity) error { + return u.repository.Delete(entity) +} diff --git a/internal/modules/module/module.go b/internal/modules/module/module.go index 527cf27..1e6a37b 100644 --- a/internal/modules/module/module.go +++ b/internal/modules/module/module.go @@ -3,11 +3,12 @@ package module import ( + "gorm.io/gorm" "udap/internal/core/domain" ) -func New() domain.ModuleService { - repo := NewRepository() +func New(db *gorm.DB) domain.ModuleService { + repo := NewRepository(db) service := NewService(repo) return service } diff --git a/internal/modules/module/repository.go b/internal/modules/module/repository.go index e8786a8..37bd2a4 100644 --- a/internal/modules/module/repository.go +++ b/internal/modules/module/repository.go @@ -3,27 +3,68 @@ package module import ( + "gorm.io/gorm" "udap/internal/core/domain" ) type moduleRepo struct { + db *gorm.DB } -func NewRepository() domain.ModuleRepository { - return &moduleRepo{} +func NewRepository(db *gorm.DB) domain.ModuleRepository { + return &moduleRepo{ + db: db, + } } -func (u moduleRepo) Candidates() ([]string, error) { - // TODO implement me - panic("implement me") +func (m moduleRepo) FindByName(name string) (*domain.Module, error) { + var target domain.Module + if err := m.db.Where("name = ?", name).First(&target).Error; err != nil { + return nil, err + } + return &target, nil } -func (u moduleRepo) FindAll() ([]*domain.Module, error) { - // TODO implement me - panic("implement me") +func (m moduleRepo) FindAll() (*[]domain.Module, error) { + var targets []domain.Module + if err := m.db.Find(&targets).Error; err != nil { + return nil, err + } + return &targets, nil } -func (u moduleRepo) FindByName(name string) (*domain.Module, error) { - // TODO implement me - panic("implement me") +func (m moduleRepo) FindById(id string) (*domain.Module, error) { + var target *domain.Module + if err := m.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (m moduleRepo) Create(module *domain.Module) error { + if err := m.db.Create(module).Error; err != nil { + return err + } + return nil +} + +func (m moduleRepo) FindOrCreate(module *domain.Module) error { + if err := m.db.FirstOrCreate(module).Error; err != nil { + return err + } + return nil +} + +func (m moduleRepo) Update(module *domain.Module) error { + if err := m.db.Save(module).Error; err != nil { + return err + } + return nil +} + +func (m moduleRepo) Delete(module *domain.Module) error { + if err := m.db.Delete(module).Error; err != nil { + return err + } + return nil } diff --git a/internal/modules/module/service.go b/internal/modules/module/service.go index 0e5c978..898ac55 100644 --- a/internal/modules/module/service.go +++ b/internal/modules/module/service.go @@ -3,35 +3,101 @@ package module import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "time" "udap/internal/core/domain" + "udap/internal/log" ) +const DIR = "modules" + type moduleService struct { repository domain.ModuleRepository } -func (u moduleService) Discover() error { - // TODO implement me - panic("implement me") +func NewService(repository domain.ModuleRepository) domain.ModuleService { + return moduleService{repository: repository} } -func (u moduleService) Build(name string) error { - // TODO implement me - panic("implement me") +func (u moduleService) Discover() error { + // Format the pattern for glob search + pattern := fmt.Sprintf("./%s/*/*.go", DIR) + // Run the search for go files + files, err := filepath.Glob(pattern) + if err != nil { + return err + } + // Launch a go func to build each one + for _, p := range files { + name := strings.Replace(filepath.Base(p), ".go", "", 1) + var target *domain.Module + target, err = u.repository.FindByName(name) + if err != nil { + target.Name = name + target.Path = p + err = u.repository.Create(target) + if err != nil { + return err + } + } + } + return nil } -func (u moduleService) BuildAll() error { - // TODO implement me - panic("implement me") +func (u moduleService) Build(module *domain.Module) error { + start := time.Now() + if _, err := os.Stat(module.Path); err != nil { + return err + } + // Create a timeout to prevent modules from taking too long to build + timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*15) + // Cancel the timeout of it exits before the timeout is up + defer cancelFunc() + binary := strings.Replace(module.Path, ".go", ".so", 1) + // Prepare the command arguments + args := []string{"build", "-v", "-buildmode=plugin", "-o", binary, module.Path} + // Initialize the command structure + cmd := exec.CommandContext(timeout, "go", args...) + // Run and get the stdout and stderr from the output + output, err := cmd.CombinedOutput() + if err != nil { + log.ErrF(errors.New(string(output)), "Module '%s' build failed:", module.Name) + return nil + } + log.Event("Module '%s' compiled successfully (%s)", module.Name, time.Since(start).Truncate(time.Millisecond).String()) + return nil } -func NewService(repository domain.ModuleRepository) domain.ModuleService { - return moduleService{repository: repository} +func (u moduleService) BuildAll() error { + modules, err := u.repository.FindAll() + if err != nil { + return err + } + wg := sync.WaitGroup{} + wg.Add(len(*modules)) + for _, module := range *modules { + go func(mod domain.Module) { + defer wg.Done() + err = u.Build(&mod) + if err != nil { + log.Err(err) + } + }(module) + } + wg.Wait() + return nil } // Repository Mapping -func (u moduleService) FindAll() ([]*domain.Module, error) { +func (u moduleService) FindAll() (*[]domain.Module, error) { return u.repository.FindAll() } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go new file mode 100644 index 0000000..fd985ba --- /dev/null +++ b/internal/orchestrator/orchestrator.go @@ -0,0 +1,71 @@ +// Copyright (c) 2022 Braden Nicholson + +package orchestrator + +import ( + "github.com/go-chi/chi" + "gorm.io/gorm" + "net/http" + "udap/internal/controller" + "udap/internal/log" + "udap/internal/port/routes" + "udap/internal/port/runtimes" + "udap/platform/database" + "udap/platform/router" +) + +type orchestrator struct { + db *gorm.DB + router chi.Router + controller *controller.Controller +} + +func (o orchestrator) Run() error { + + server := &http.Server{Addr: ":8080", Handler: o.router} + + err := server.ListenAndServe() + if err != nil { + log.ErrF(err, "http server exited with error:\n") + } + + return nil +} + +type Orchestrator interface { + Init() error + Run() error +} + +func NewOrchestrator() Orchestrator { + // Initialize Database + db, err := database.New() + if err != nil { + return nil + } + // Initialize Router + r := router.New() + + return &orchestrator{ + db: db, + router: r, + controller: nil, + } +} + +func (o orchestrator) Init() error { + var err error + o.controller, err = controller.NewController(o.db) + if err != nil { + return err + } + + // Initialize and route applicable domains + routes.NewUserRouter(o.controller.Users).RouteUsers(o.router) + routes.NewEndpointRouter(o.controller.Endpoints).RouteEndpoints(o.router) + routes.NewModuleRouter(o.controller.Modules).RouteModules(o.router) + + runtimes.NewModuleRuntime(o.controller.Modules) + + return nil +} diff --git a/internal/port/routes/endpoint.go b/internal/port/routes/endpoint.go new file mode 100644 index 0000000..1d601a3 --- /dev/null +++ b/internal/port/routes/endpoint.go @@ -0,0 +1,68 @@ +// Copyright (c) 2022 Braden Nicholson + +package routes + +import ( + "encoding/json" + "github.com/go-chi/chi" + "net/http" + "udap/internal/core/domain" + "udap/platform/jwt" +) + +type EndpointRouter interface { + RouteEndpoints(router chi.Router) +} + +type endpointRouter struct { + service domain.EndpointService +} + +func NewEndpointRouter(service domain.EndpointService) EndpointRouter { + return endpointRouter{ + service: service, + } +} + +func (r endpointRouter) RouteEndpoints(router chi.Router) { + router.Route("/endpoints", func(local chi.Router) { + local.Post("/authenticate/{key}", r.authenticate) + }) +} + +type authenticationResponse struct { + Token string `json:"token"` +} + +func (r endpointRouter) authenticate(w http.ResponseWriter, req *http.Request) { + key := chi.URLParam(req, "key") + if key == "" { + http.Error(w, "access key not provided", 401) + } + + endpoint, err := r.service.FindByKey(key) + if err != nil { + http.Error(w, "invalid endpoint name", 401) + } + + token, err := jwt.SignUUID(endpoint.Id) + if err != nil { + http.Error(w, "Failed to generate JWT.", 500) + return + } + + resolve := authenticationResponse{} + resolve.Token = token + + marshal, err := json.Marshal(resolve) + if err != nil { + http.Error(w, "Failed to generate json...", 500) + return + } + + _, err = w.Write(marshal) + if err != nil { + return + } + w.WriteHeader(200) +} diff --git a/internal/port/rest/module.go b/internal/port/routes/module.go similarity index 93% rename from internal/port/rest/module.go rename to internal/port/routes/module.go index b3cf126..38af2f8 100644 --- a/internal/port/rest/module.go +++ b/internal/port/routes/module.go @@ -1,6 +1,6 @@ // Copyright (c) 2022 Braden Nicholson -package rest +package routes import ( "github.com/go-chi/chi" @@ -36,7 +36,11 @@ func (r moduleRouter) RouteModules(router chi.Router) { func (r moduleRouter) build(w http.ResponseWriter, req *http.Request) { name := chi.URLParam(req, "name") if name != "" { - err := r.service.Build(name) + mod, err := r.service.FindByName(name) + if err != nil { + return + } + err = r.service.Build(mod) if err != nil { http.Error(w, "invalid module name", 401) } diff --git a/internal/port/routes/user.go b/internal/port/routes/user.go new file mode 100644 index 0000000..bb61e82 --- /dev/null +++ b/internal/port/routes/user.go @@ -0,0 +1,54 @@ +// Copyright (c) 2022 Braden Nicholson + +package routes + +import ( + "bytes" + "encoding/json" + "github.com/go-chi/chi" + "net/http" + "udap/internal/core/domain" +) + +type UserRouter interface { + RouteUsers(router chi.Router) +} + +type userRouter struct { + service domain.UserService +} + +func NewUserRouter(service domain.UserService) UserRouter { + return userRouter{ + service: service, + } +} + +func (r userRouter) RouteUsers(router chi.Router) { + router.Route("/users", func(local chi.Router) { + local.Post("/register", r.register) + }) +} + +func (r userRouter) register(w http.ResponseWriter, req *http.Request) { + + var buf bytes.Buffer + + _, err := buf.ReadFrom(req.Body) + if err != nil { + return + } + ref := domain.User{} + err = json.Unmarshal(buf.Bytes(), &ref) + if err != nil { + http.Error(w, "could not parse user", 400) + return + } + + err = r.service.Register(&ref) + if err != nil { + http.Error(w, "failed to create user", 400) + return + } + w.WriteHeader(200) +} diff --git a/internal/port/runtimes/module.go b/internal/port/runtimes/module.go new file mode 100644 index 0000000..532961b --- /dev/null +++ b/internal/port/runtimes/module.go @@ -0,0 +1,22 @@ +// Copyright (c) 2022 Braden Nicholson + +package runtimes + +import ( + "udap/internal/core/domain" + "udap/internal/log" +) + +func NewModuleRuntime(service domain.ModuleService) { + err := service.Discover() + if err != nil { + return + } + + err = service.BuildAll() + if err != nil { + log.Err(err) + return + } + +} diff --git a/internal/port/rest/user.go b/internal/udap/runner.go similarity index 55% rename from internal/port/rest/user.go rename to internal/udap/runner.go index cb76889..d3e178d 100644 --- a/internal/port/rest/user.go +++ b/internal/udap/runner.go @@ -1,3 +1,7 @@ // Copyright (c) 2022 Braden Nicholson -package rest +package udap + +func Begin() { + +} diff --git a/modules/homekit/homekit.go b/modules/homekit/homekit.go index d933f50..b8c614a 100644 --- a/modules/homekit/homekit.go +++ b/modules/homekit/homekit.go @@ -10,9 +10,8 @@ import ( "github.com/brutella/hc/service" "os" "time" - "udap/internal/controller" + "udap/internal/core/domain" "udap/internal/log" - "udap/internal/models" "udap/pkg/plugin" ) @@ -66,10 +65,11 @@ func (h *Homekit) Run() error { var accessories []*accessory.Accessory - keys := h.Entities.Keys() + entities, err := h.Entities.FindAll() - for _, name := range keys { - entity := *h.Entities.Find(name) + keys := *entities + + for _, entity := range keys { switch entity.Type { case "spectrum": info := accessory.Info{ @@ -81,10 +81,12 @@ func (h *Homekit) Run() error { FirmwareRevision: h.Module.Version, } device := newSpectrumLight(info) - err := device.syncAttributes(h.Attributes, entity.Id) + + err = device.syncAttributes(h.Attributes, entity.Id) if err != nil { return err } + accessories = append(accessories, device.Accessory) case "switch": info := accessory.Info{ @@ -121,7 +123,7 @@ func (h *Homekit) Run() error { return nil } -func syncSwitch(p *service.Switch, a *controller.Attributes, id string) { +func syncSwitch(p *service.Switch, a domain.AttributeService, id string) { p.On.OnValueRemoteUpdate(func(b bool) { str := "false" if b { @@ -133,11 +135,11 @@ func syncSwitch(p *service.Switch, a *controller.Attributes, id string) { } }) - a.WatchSingle(fmt.Sprintf("%s.%s", id, "on"), func(data interface{}) error { - attr := *data.(*models.Attribute) - p.On.UpdateValue(attr.Request) - return nil - }) + // a.WatchSingle(fmt.Sprintf("%s.%s", id, "on"), func(data interface{}) error { + // attr := *data.(*models.Attribute) + // p.On.UpdateValue(attr.Request) + // return nil + // }) } @@ -146,12 +148,13 @@ type spectrumLight struct { spectrum *spectrum } -func (s *spectrumLight) syncAttributes(a *controller.Attributes, id string) error { +func (s *spectrumLight) syncAttributes(a domain.AttributeService, id string) error { s.spectrum.On.OnValueRemoteUpdate(func(b bool) { str := "false" if b { str = "true" } + err := a.Request(id, "on", str) if err != nil { log.Err(err) @@ -165,17 +168,17 @@ func (s *spectrumLight) syncAttributes(a *controller.Attributes, id string) erro } }) - a.WatchSingle(fmt.Sprintf("%s.%s", id, "on"), func(data interface{}) error { - attr := *data.(*models.Attribute) - s.spectrum.On.UpdateValue(attr.Request) - return nil - }) - - a.WatchSingle(fmt.Sprintf("%s.%s", id, "dim"), func(data interface{}) error { - attr := *data.(*models.Attribute) - s.spectrum.Dim.UpdateValue(attr.Request) - return nil - }) + // a.WatchSingle(fmt.Sprintf("%s.%s", id, "on"), func(data interface{}) error { + // attr := *data.(*models.Attribute) + // s.spectrum.On.UpdateValue(attr.Request) + // return nil + // }) + // + // a.WatchSingle(fmt.Sprintf("%s.%s", id, "dim"), func(data interface{}) error { + // attr := *data.(*models.Attribute) + // s.spectrum.Dim.UpdateValue(attr.Request) + // return nil + // }) return nil } diff --git a/modules/hs100/hs100.go b/modules/hs100/hs100.go index 9d04cc0..782e806 100644 --- a/modules/hs100/hs100.go +++ b/modules/hs100/hs100.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "time" + "udap/internal/core/domain" "udap/internal/models" "udap/pkg/plugin" ) @@ -47,25 +48,36 @@ func (h *HS100) findDevices() error { return err } - newSwitch := models.NewSwitch(strings.ToLower(name), "hs100") - - _, err = h.Entities.Register(newSwitch) + newSwitch := domain.Entity{ + Name: strings.ToLower(name), + Type: "switch", + Module: "hs100", + } + _, err = h.Entities.Register(&newSwitch) if err != nil { return err } h.devices[newSwitch.Id] = device - on := &models.Attribute{ + channel := make(chan domain.Attribute) + on := &domain.Attribute{ Key: "on", Value: "false", Request: "false", Order: 0, Type: "toggle", Entity: newSwitch.Id, + Channel: channel, } - on.FnGet(h.get(device)) - on.FnPut(h.put(device)) + go func() { + for attribute := range channel { + err = h.put(device)(attribute.Request) + if err != nil { + return + } + } + }() err = h.Attributes.Register(on) if err != nil { diff --git a/modules/macmeta/macmeta.go b/modules/macmeta/macmeta.go index 2437a3c..b38edfd 100644 --- a/modules/macmeta/macmeta.go +++ b/modules/macmeta/macmeta.go @@ -4,7 +4,7 @@ package main import ( "os/exec" - "udap/internal/models" + "udap/internal/core/domain" "udap/pkg/plugin" ) @@ -27,42 +27,41 @@ func init() { } func (v *MacMeta) createDisplaySwitch() error { - newSwitch := models.NewSwitch("terminal", "macmeta") + + newSwitch := &domain.Entity{ + Name: "terminal", + Type: "switch", + Module: "macmeta", + } _, err := v.Entities.Register(newSwitch) if err != nil { return err } - on := &models.Attribute{ + on := &domain.Attribute{ Key: "on", Value: "true", Request: "true", Type: "toggle", + Channel: make(chan domain.Attribute), Order: 0, Entity: newSwitch.Id, } - on.FnGet(func() (string, error) { - if v.localDisplay { - return "true", nil - } else { - return "false", nil - } - }) - - on.FnPut(func(value string) error { - if value == "true" { - err = v.displayOn() - if err != nil { - return err - } - } else { - err = v.displayOff() - if err != nil { - return err + go func() { + for attribute := range on.Channel { + if attribute.Request == "true" { + err = v.displayOn() + if err != nil { + continue + } + } else { + err = v.displayOff() + if err != nil { + continue + } } } - return nil - }) + }() err = v.Attributes.Register(on) if err != nil { diff --git a/modules/spotify/spotify.go b/modules/spotify/spotify.go index b2294b2..beabe79 100644 --- a/modules/spotify/spotify.go +++ b/modules/spotify/spotify.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "time" + "udap/internal/core/domain" "udap/internal/log" "udap/internal/models" "udap/pkg/plugin" @@ -307,49 +308,74 @@ func (s *Spotify) push() error { } func (s *Spotify) Run() error { - e := models.NewMediaEntity("Remote", "spotify") + e := &domain.Entity{ + Name: "remote", + Type: "media", + Module: "spotify", + } _, err := s.Entities.Register(e) if err != nil { return err } - current := &models.Attribute{ + current := &domain.Attribute{ Key: "current", Value: "{}", Request: "{}", Entity: e.Id, + Channel: make(chan domain.Attribute), } - current.FnGet(s.GetAttribute(current.Key)) - current.FnPut(s.PutAttribute(current.Key)) + go func() { + for attribute := range current.Channel { + err := s.PutAttribute(current.Key)(attribute.Request) + if err != nil { + return + } + } + }() err = s.Attributes.Register(current) if err != nil { return err } - playing := &models.Attribute{ + playing := &domain.Attribute{ Key: "playing", Value: "false", Request: "false", Entity: e.Id, + Channel: make(chan domain.Attribute), } + go func() { + for attribute := range playing.Channel { + err := s.PutAttribute(playing.Key)(attribute.Request) + if err != nil { + return + } + } + }() - playing.FnGet(s.GetAttribute(playing.Key)) - playing.FnPut(s.PutAttribute(playing.Key)) err = s.Attributes.Register(playing) if err != nil { return err } - cmd := &models.Attribute{ + cmd := &domain.Attribute{ Key: "cmd", Value: "none", Request: "none", Entity: e.Id, + Channel: make(chan domain.Attribute), } + go func() { + for attribute := range cmd.Channel { + err := s.PutAttribute(cmd.Key)(attribute.Request) + if err != nil { + return + } + } + }() - cmd.FnGet(s.GetAttribute(cmd.Key)) - cmd.FnPut(s.PutAttribute(cmd.Key)) err = s.Attributes.Register(cmd) if err != nil { return err @@ -376,7 +402,7 @@ func (s *Spotify) Run() error { if err != nil { return err } - _, err = s.Entities.Config(s.id, string(marshal)) + err = s.Entities.Config(s.id, string(marshal)) if err != nil { return err } diff --git a/modules/weather/weather.go b/modules/weather/weather.go index 4ad7dda..82c80fb 100644 --- a/modules/weather/weather.go +++ b/modules/weather/weather.go @@ -7,7 +7,7 @@ import ( "encoding/json" "net/http" "time" - "udap/internal/models" + "udap/internal/core/domain" "udap/pkg/plugin" ) @@ -176,7 +176,11 @@ func (v *Weather) Run() error { return err } - e := models.NewMediaEntity("weather", "weather") + e := &domain.Entity{ + Name: "weather", + Module: "weather", + Type: "media", + } _, err = v.Entities.Register(e) if err != nil { return err @@ -185,7 +189,7 @@ func (v *Weather) Run() error { if err != nil { return err } - forecast := models.Attribute{ + forecast := domain.Attribute{ Key: "forecast", Value: buffer, Request: buffer, @@ -195,14 +199,6 @@ func (v *Weather) Run() error { } v.eId = e.Id - forecast.FnGet(func() (string, error) { - return v.forecastBuffer() - }) - - forecast.FnPut(func(value string) error { - return nil - }) - err = v.Attributes.Register(&forecast) if err != nil { return err diff --git a/platform/database/database.go b/platform/database/database.go new file mode 100644 index 0000000..e7962b6 --- /dev/null +++ b/platform/database/database.go @@ -0,0 +1,36 @@ +// Copyright (c) 2021 Braden Nicholson + +package database + +import ( + "fmt" + "gorm.io/driver/postgres" + _ "gorm.io/driver/postgres" + "gorm.io/gorm" + "os" +) + +func New() (*gorm.DB, error) { + pg := postgres.Open(dbURL()) + db, err := gorm.Open(pg, &gorm.Config{}) + if err != nil { + return nil, err + } + + return db, nil +} + +// dbURL returns a formatted postgresql connection string. +func dbURL() string { + // The credentials are retrieved from the OS environment + dbUser := os.Getenv("dbUser") + dbPass := os.Getenv("dbPass") + // Host and port are also obtained from the environment + dbHost := os.Getenv("dbHost") + dbPort := os.Getenv("dbPort") + // The name of the database is again retrieved from the environment + dbName := os.Getenv("dbName") + // All variables are aggregated into the connection url + u := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", dbHost, dbUser, dbPass, dbName, dbPort) + return u +} diff --git a/platform/jwt/auth.go b/platform/jwt/auth.go new file mode 100644 index 0000000..41c5a26 --- /dev/null +++ b/platform/jwt/auth.go @@ -0,0 +1,47 @@ +// Copyright (c) 2022 Braden Nicholson + +package jwt + +import ( + "fmt" + "github.com/go-chi/jwtauth/v5" + "net/http" + "os" +) + +var tokenAuth *jwtauth.JWTAuth + +func LoadKeys() { + privateKey := os.Getenv("private") + tokenAuth = jwtauth.New("HS512", []byte(privateKey), nil) +} + +func AuthToken(token string) (string, error) { + content, err := jwtauth.VerifyToken(tokenAuth, token) + if err != nil { + return "", err + } + + val, ok := content.Get("id") + if !ok { + return "", fmt.Errorf("malformed jwt... This is a serious concern") + } + + s := val.(string) + + return s, nil +} + +func VerifyToken() func(http.Handler) http.Handler { + return jwtauth.Verifier(tokenAuth) +} + +func SignUUID(id string) (string, error) { + claims := map[string]any{} + claims["id"] = id + _, s, err := tokenAuth.Encode(claims) + if err != nil { + return s, err + } + return s, nil +} diff --git a/platform/router/auth.go b/platform/router/auth.go new file mode 100644 index 0000000..6e516c4 --- /dev/null +++ b/platform/router/auth.go @@ -0,0 +1,47 @@ +// Copyright (c) 2022 Braden Nicholson + +package router + +import ( + "fmt" + "github.com/go-chi/jwtauth/v5" + "net/http" + "os" +) + +var tokenAuth *jwtauth.JWTAuth + +func loadKeys() { + privateKey := os.Getenv("private") + tokenAuth = jwtauth.New("HS512", []byte(privateKey), nil) +} + +func authToken(token string) (string, error) { + content, err := jwtauth.VerifyToken(tokenAuth, token) + if err != nil { + return "", err + } + + val, ok := content.Get("id") + if !ok { + return "", fmt.Errorf("malformed jwt... This is a serious concern") + } + + s := val.(string) + + return s, nil +} + +func verifyToken() func(http.Handler) http.Handler { + return jwtauth.Verifier(tokenAuth) +} + +func signUUID(id string) (string, error) { + claims := map[string]any{} + claims["id"] = id + _, s, err := tokenAuth.Encode(claims) + if err != nil { + return s, err + } + return s, nil +} diff --git a/platform/router/router.go b/platform/router/router.go new file mode 100644 index 0000000..725703b --- /dev/null +++ b/platform/router/router.go @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Braden Nicholson + +package router + +import ( + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/cors" + "net/http" + "udap/platform/jwt" +) + +func New() chi.Router { + router := chi.NewRouter() + + router.Use(middleware.Recoverer) + // Custom Middleware + router.Use(corsHeaders()) + // Status Middleware + router.Use(middleware.Heartbeat("/status")) + // Seek, verify and validate JWT tokens + router.Use(jwt.VerifyToken()) + // Load JWT Keys + jwt.LoadKeys() + return router +} + +func corsHeaders() func(next http.Handler) http.Handler { + return cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Bond"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by interface{} of major browsers + }) +} From 2f4245210c8290cb370ed8e289e790c69c2d7824 Mon Sep 17 00:00:00 2001 From: Braden Date: Sat, 28 May 2022 19:39:41 -0700 Subject: [PATCH 04/18] Migrates the remaining models to the new structure. Cleaned up old models. Began work on reinstating endpoints. Basic reactive channel-based updates instead of the function based in the previous implementation. --- cmd/udap/main.go | 2 +- internal/bond/bond.go | 6 +- internal/controller/attribute.go | 165 ---------------- internal/controller/controller.go | 30 +-- internal/controller/device.go | 85 -------- internal/controller/endpoint.go | 116 ----------- internal/controller/entity.go | 187 ------------------ internal/controller/modules.go | 138 ------------- internal/controller/network.go | 92 --------- internal/controller/user.go | 103 ---------- internal/controller/zone.go | 139 ------------- internal/core/domain/attribute.go | 57 +++++- internal/core/domain/attribute_test.go | 43 ++++ internal/core/domain/device.go | 1 + internal/core/domain/endpoint.go | 40 +++- internal/core/domain/entity.go | 3 +- internal/core/domain/module.go | 13 ++ internal/core/domain/network.go | 34 ++++ internal/core/domain/zone.go | 28 +++ internal/core/migrate.go | 17 ++ internal/models/attribute.go | 112 ----------- internal/models/attribute_test.go | 116 ----------- internal/models/device.go | 49 ----- internal/models/endpoint.go | 167 ---------------- internal/models/entity.go | 187 ------------------ internal/models/log.go | 16 -- internal/models/migrate.go | 13 -- internal/models/module.go | 75 ------- internal/models/network.go | 34 ---- internal/models/remote.go | 64 ------ internal/models/user.go | 73 ------- internal/models/zone.go | 47 ----- internal/modules/attribute/attribute.go | 3 +- internal/modules/attribute/operator.go | 67 +++++++ internal/modules/attribute/repository.go | 39 +++- internal/modules/attribute/service.go | 85 ++++++-- internal/modules/device/device.go | 14 ++ internal/modules/device/repository.go | 62 ++++++ internal/modules/device/service.go | 45 +++++ internal/modules/endpoint/endpoint.go | 3 +- internal/modules/endpoint/operator.go | 69 +++++++ internal/modules/endpoint/repository.go | 20 +- internal/modules/endpoint/service.go | 29 ++- .../{server => modules/endpoint}/system.go | 9 +- internal/modules/entity/repository.go | 25 ++- internal/modules/entity/service.go | 11 +- internal/modules/module/module.go | 6 +- internal/modules/module/operator.go | 100 ++++++++++ internal/modules/module/service.go | 119 ++++++++--- internal/modules/network/network.go | 14 ++ internal/modules/network/repository.go | 89 +++++++++ internal/modules/network/service.go | 45 +++++ internal/modules/zone/repository.go | 62 ++++++ internal/modules/zone/service.go | 41 ++++ internal/modules/zone/zone.go | 14 ++ internal/orchestrator/orchestrator.go | 109 +++++++++- internal/port/routes/endpoint.go | 40 +++- internal/port/runtimes/module.go | 10 + internal/store/database.go | 54 ----- internal/udap/runner.go | 7 - internal/udap/udap.go | 72 ------- modules/govee/govee.go | 40 ++-- modules/hs100/hs100.go | 7 +- modules/macmeta/macmeta.go | 2 +- modules/spotify/spotify.go | 7 +- modules/squid/squid.go | 44 +++-- modules/vyos/vyos.go | 13 +- modules/weather/weather.go | 2 +- .../database}/database_test.go | 4 +- 69 files changed, 1355 insertions(+), 2279 deletions(-) delete mode 100644 internal/controller/attribute.go delete mode 100644 internal/controller/device.go delete mode 100644 internal/controller/endpoint.go delete mode 100644 internal/controller/entity.go delete mode 100644 internal/controller/modules.go delete mode 100644 internal/controller/network.go delete mode 100644 internal/controller/user.go delete mode 100644 internal/controller/zone.go create mode 100644 internal/core/domain/attribute_test.go create mode 100644 internal/core/domain/network.go create mode 100644 internal/core/domain/zone.go create mode 100644 internal/core/migrate.go delete mode 100644 internal/models/attribute.go delete mode 100644 internal/models/attribute_test.go delete mode 100644 internal/models/device.go delete mode 100644 internal/models/endpoint.go delete mode 100644 internal/models/entity.go delete mode 100644 internal/models/log.go delete mode 100644 internal/models/migrate.go delete mode 100644 internal/models/module.go delete mode 100644 internal/models/network.go delete mode 100644 internal/models/remote.go delete mode 100644 internal/models/user.go delete mode 100644 internal/models/zone.go create mode 100644 internal/modules/attribute/operator.go create mode 100644 internal/modules/device/device.go create mode 100644 internal/modules/device/repository.go create mode 100644 internal/modules/device/service.go create mode 100644 internal/modules/endpoint/operator.go rename internal/{server => modules/endpoint}/system.go (94%) create mode 100644 internal/modules/module/operator.go create mode 100644 internal/modules/network/network.go create mode 100644 internal/modules/network/repository.go create mode 100644 internal/modules/network/service.go create mode 100644 internal/modules/zone/repository.go create mode 100644 internal/modules/zone/service.go create mode 100644 internal/modules/zone/zone.go delete mode 100644 internal/store/database.go delete mode 100644 internal/udap/runner.go delete mode 100644 internal/udap/udap.go rename {internal/store => platform/database}/database_test.go (89%) diff --git a/cmd/udap/main.go b/cmd/udap/main.go index 030005f..bcf7da0 100644 --- a/cmd/udap/main.go +++ b/cmd/udap/main.go @@ -23,7 +23,7 @@ func main() { o := orchestrator.NewOrchestrator() // Initialize services - err = o.Init() + err = o.Start() if err != nil { return } diff --git a/internal/bond/bond.go b/internal/bond/bond.go index bdb980d..6bd33d2 100644 --- a/internal/bond/bond.go +++ b/internal/bond/bond.go @@ -74,9 +74,9 @@ func (m *Msg) Success() { } type Resp struct { - Success bool `json:"status"` - Error string `json:"error"` - Body interface{} `json:"body"` + Success bool `json:"status"` + Error string `json:"error"` + Body any `json:"body"` } type Bond struct { diff --git a/internal/controller/attribute.go b/internal/controller/attribute.go deleted file mode 100644 index df77076..0000000 --- a/internal/controller/attribute.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package controller - -import ( - "encoding/json" - "fmt" - "sync" - "time" - "udap/internal/bond" - "udap/internal/models" -) - -type Attributes struct { - PolyBuffer - Observable -} - -func (a *Attributes) Handle(event bond.Msg) (res interface{}, err error) { - switch event.Operation { - case "request": - return a.request(event) - default: - return nil, fmt.Errorf("operaiton not found") - } -} - -func (a *Attributes) Register(attribute *models.Attribute) error { - attribute.Id = attribute.Path() - a.Store(attribute) - return nil -} - -func (a *Attributes) Request(entity string, key string, value string) error { - attr := models.Attribute{} - attr.Entity = entity - attr.Key = key - attribute := a.Find(attr.Path()) - if attribute == nil { - return fmt.Errorf("attribute '%s' not found", attr.Id) - } - - err := attribute.SendRequest(value) - if err != nil { - return err - } - - a.Store(attribute) - - return nil -} - -func (a *Attributes) request(event bond.Msg) (interface{}, error) { - attr := models.Attribute{} - err := json.Unmarshal([]byte(event.Payload), &attr) - if err != nil { - return nil, err - } - attribute := a.Find(attr.Path()) - if attribute == nil { - return nil, fmt.Errorf("attribute '%s' not found", attr.Id) - } - - err = attribute.SendRequest(attr.Request) - if err != nil { - return nil, err - } - - a.Store(attribute) - - return nil, nil -} - -func (a *Attributes) EmitAll() (err error) { - - for _, k := range a.Keys() { - find := a.Find(k) - a.emit(k, find) - } - - return nil -} - -func (a *Attributes) Set(entity string, key string, value string) error { - attr := models.Attribute{} - attr.Entity = entity - attr.Key = key - - attribute := a.Find(attr.Path()) - if attribute == nil { - return fmt.Errorf("attribute not found") - } - - attribute.SetValue(value) - - a.Store(attribute) - - return nil -} - -func (a *Attributes) Update(entity string, key string, value string, stamp time.Time) error { - attr := models.Attribute{} - attr.Entity = entity - attr.Key = key - - attribute := a.Find(attr.Path()) - if attribute == nil { - return fmt.Errorf("attribute not found") - } - - err := attribute.UpdateValue(value, stamp) - if err != nil { - return err - } - - a.Store(attribute) - - return nil -} - -func (a *Attributes) Query(entity string, key string) string { - attr := models.Attribute{} - attr.Entity = entity - attr.Key = key - - attribute := a.Find(attr.Path()) - if attribute == nil { - return "" - } - - return attribute.Request -} - -func (a *Attributes) Compile() []models.Attribute { - var attributes []models.Attribute - for _, key := range a.Keys() { - attribute := a.Find(key) - if attribute == nil { - continue - } - attributes = append(attributes, *attribute) - } - return attributes -} - -func (a *Attributes) Find(name string) *models.Attribute { - res := a.get(name) - val, ok := res.(*models.Attribute) - if !ok { - return nil - } - return val -} - -func (a *Attributes) Store(attribute *models.Attribute) { - a.set(attribute.Id, attribute) - a.emit(attribute.Id, attribute) -} - -func LoadAttributes() (m *Attributes) { - m = &Attributes{} - m.data = sync.Map{} - m.Run() - return m -} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index b01e3f5..a1d1d20 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -8,34 +8,36 @@ import ( "udap/internal/bond" "udap/internal/core/domain" "udap/internal/modules/attribute" + "udap/internal/modules/device" "udap/internal/modules/endpoint" "udap/internal/modules/entity" - "udap/internal/modules/module" + "udap/internal/modules/network" "udap/internal/modules/user" + "udap/internal/modules/zone" "udap/internal/pulse" ) type Controller struct { - Devices *Devices - Zones *Zones - Networks *Networks - Attributes domain.AttributeService - Modules domain.ModuleService - Entities domain.EntityService - Endpoints domain.EndpointService - ModuleService domain.ModuleService - Users domain.UserService - event chan bond.Msg + Attributes domain.AttributeService + Devices domain.DeviceService + Endpoints domain.EndpointService + Entities domain.EntityService + Networks domain.NetworkService + Users domain.UserService + Zones domain.ZoneService + event chan bond.Msg } func NewController(db *gorm.DB) (*Controller, error) { c := &Controller{} - c.Users = user.New(db) - c.Endpoints = endpoint.New(db) c.Attributes = attribute.New(db) + c.Devices = device.New(db) + c.Endpoints = endpoint.New(db) c.Entities = entity.New(db) - c.Modules = module.New(db) + c.Networks = network.New(db) + c.Users = user.New(db) + c.Zones = zone.New(db) return c, nil } diff --git a/internal/controller/device.go b/internal/controller/device.go deleted file mode 100644 index 94cdd44..0000000 --- a/internal/controller/device.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package controller - -import ( - "sync" - "udap/internal/bond" - "udap/internal/log" - "udap/internal/models" - "udap/internal/store" -) - -type Devices struct { - PolyBuffer - Observable -} - -func (d *Devices) Handle(event bond.Msg) (res interface{}, err error) { - switch event.Operation { - } - return nil, nil -} - -func (d *Devices) Compile() (res []models.Device, err error) { - for _, s := range d.Keys() { - device := d.Find(s) - if device == nil { - continue - } - - res = append(res, *device) - } - return res, nil -} - -func (d *Devices) EmitAll() (err error) { - - for _, k := range d.Keys() { - find := d.Find(k) - d.emit(k, find) - } - - return nil -} - -func (d *Devices) FetchAll() { - var devices []models.Device - err := store.DB.Model(&models.Device{}).Find(&devices).Error - if err != nil { - log.Err(err) - return - } - for _, device := range devices { - - d.set(device.Id, &device) - d.emit(device.Id, &device) - } -} - -func (d *Devices) Find(name string) *models.Device { - return d.get(name).(*models.Device) -} - -func LoadDevices() (m *Devices) { - m = &Devices{} - m.data = sync.Map{} - m.Run() - m.FetchAll() - return m -} - -func (d *Devices) Register(device models.Device) (res *models.Device, err error) { - err = device.Emplace() - if err != nil { - return nil, err - } - d.set(device.Id, &device) - d.emit(device.Id, &device) - return nil, nil -} - -func (d *Devices) Set(id string, device *models.Device) { - d.set(id, device) - d.emit(device.Id, &device) -} diff --git a/internal/controller/endpoint.go b/internal/controller/endpoint.go deleted file mode 100644 index 505322e..0000000 --- a/internal/controller/endpoint.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package controller - -import ( - "fmt" - "github.com/go-chi/chi" - "sync" - "udap/internal/bond" - "udap/internal/log" - "udap/internal/models" - "udap/internal/store" -) - -type Response struct { - Id string `json:"id"` - Status string `json:"status"` - Operation string `json:"operation"` - Body interface{} `json:"body"` -} - -type Endpoints struct { - PolyBuffer - Observable - bond *bond.Bond - router chi.Router -} - -func (e *Endpoints) Handle(msg bond.Msg) (res interface{}, err error) { - switch t := msg.Operation; t { - case "create": - return e.create(msg) - default: - return nil, fmt.Errorf("operation '%s' is not defined", t) - } -} - -func LoadEndpoints() (m *Endpoints) { - m = &Endpoints{} - m.data = sync.Map{} - m.raw = map[string]interface{}{} - m.Run() - m.FetchAll() - return m -} - -func (d *Endpoints) EmitAll() (err error) { - - for _, k := range d.Keys() { - find := d.Find(k) - d.emit(k, find) - } - - return nil -} - -func (e *Endpoints) FetchAll() { - var endpoints []*models.Endpoint - store.DB.Model(&models.Endpoint{}).Find(&endpoints) - for _, endpoint := range endpoints { - e.Set(endpoint.Id, endpoint) - } -} - -func (e *Endpoints) create(msg bond.Msg) (res interface{}, err error) { - ep := models.NewEndpoint(msg.Payload) - err = store.DB.Create(&ep).Error - if err != nil { - return nil, err - } - e.Set(ep.Id, &ep) - return ep, err -} - -func (e *Endpoints) unenroll(msg bond.Msg) (res interface{}, err error) { - endpoint := e.Find(msg.Id) - endpoint.Unenroll() - return nil, nil -} - -func (e *Endpoints) Compile() (endpoints []models.Endpoint, err error) { - for _, s := range e.Keys() { - endpoint := e.Find(s) - endpoints = append(endpoints, *endpoint) - } - return endpoints, err -} - -func (e *Endpoints) Find(id string) *models.Endpoint { - - dat := e.get(id) - if dat == struct{}{} { - dat = nil - } - if dat == nil { - endpoint := &models.Endpoint{} - endpoint.Id = id - err := endpoint.Fetch() - if err != nil { - log.Err(err) - } - return endpoint - } - - return dat.(*models.Endpoint) -} - -func (e *Endpoints) Set(id string, endpoint *models.Endpoint) { - e.emit(id, endpoint) - e.set(id, endpoint) - -} - -func (e *Endpoints) Save(endpoint *models.Endpoint) { - store.DB.Model(&models.Endpoint{}).Save(endpoint) -} diff --git a/internal/controller/entity.go b/internal/controller/entity.go deleted file mode 100644 index d313565..0000000 --- a/internal/controller/entity.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package controller - -import ( - "encoding/json" - "sync" - "udap/internal/bond" - "udap/internal/log" - "udap/internal/models" -) - -type Entities struct { - PolyBuffer - Observable -} - -func (e *Entities) Handle(event bond.Msg) (interface{}, error) { - switch event.Operation { - case "register": - return e.register(event) - case "rename": // Alias - return e.rename(event) - case "lock": - return e.lock(event) - case "unlock": - return e.unlock(event) - case "icon": - return e.icon(event) - case "neural": - return e.neural(event) - case "predict": - return e.predict(event) - } - return nil, nil -} - -func (e *Entities) neural(event bond.Msg) (res interface{}, err error) { - entity := e.Find(event.Id) - ref := e.Parse(event.Payload) - err = entity.ChangeNeural(ref.Neural) - if err != nil { - return nil, err - } - return nil, err -} - -func (e *Entities) EmitAll() (err error) { - - for _, k := range e.Keys() { - find := e.Find(k) - e.emit(k, find) - } - - return nil -} - -func (e *Entities) register(event bond.Msg) (interface{}, error) { - entity := e.Cast(event.Body) - - _, err := e.Register(entity) - if err != nil { - return nil, err - } - return nil, nil -} - -func (e *Entities) find(event bond.Msg) (res interface{}, err error) { - entity := e.Find(event.Id) - return entity, nil -} - -func (e *Entities) Suggest(id string, body string) (res interface{}, err error) { - entity := e.Find(id) - err = entity.Suggest(body) - if err != nil { - return nil, err - } - return nil, nil -} - -func (e *Entities) predict(event bond.Msg) (res interface{}, err error) { - entity := e.Find(event.Id) - err = entity.Suggest(string(event.Body.(json.RawMessage))) - if err != nil { - return nil, err - } - return nil, nil -} - -func (e *Entities) Cast(body interface{}) *models.Entity { - return body.(*models.Entity) -} - -func (e *Entities) Parse(body string) models.Entity { - entity := models.Entity{} - err := json.Unmarshal([]byte(body), &entity) - if err != nil { - return models.Entity{} - } - return entity -} - -func (e *Entities) Register(entity *models.Entity) (res *models.Entity, err error) { - log.Event("Entity '%s' registered.", entity.Name) - err = entity.Emplace() - if err != nil { - return nil, err - } - e.Set(entity.Id, entity) - return entity, nil -} - -func (e *Entities) rename(event bond.Msg) (res interface{}, err error) { - ref := e.Cast(event.Body) - _, err = e.Rename(event.Id, ref.Alias) - if err != nil { - return nil, err - } - return nil, nil -} - -func (e *Entities) Rename(id string, name string) (res interface{}, err error) { - entity := e.Find(id) - err = entity.Rename(name) - if err != nil { - return nil, err - } - return nil, nil -} - -func (e *Entities) lock(event bond.Msg) (res interface{}, err error) { - entity := e.Find(event.Id) - err = entity.Lock() - if err != nil { - return nil, err - } - return nil, nil -} - -func (e *Entities) unlock(event bond.Msg) (res interface{}, err error) { - entity := e.Find(event.Id) - err = entity.Unlock() - if err != nil { - return nil, err - } - return nil, nil -} - -func (e *Entities) icon(event bond.Msg) (res interface{}, err error) { - entity := e.Find(event.Id) - ee := e.Parse(event.Payload) - err = entity.ChangeIcon(ee.Icon) - if err != nil { - return nil, err - } - e.Set(event.Id, entity) - return nil, nil -} - -func (e *Entities) Config(id string, data string) (res interface{}, err error) { - entity := e.Find(id) - err = entity.ChangeConfig(data) - if err != nil { - return nil, err - } - e.Set(id, entity) - return nil, nil -} - -func LoadEntities() (m *Entities) { - m = &Entities{} - m.raw = map[string]interface{}{} - m.data = sync.Map{} - m.Run() - return m -} - -func (e *Entities) Find(name string) *models.Entity { - en := e.get(name).(*models.Entity) - return en -} - -func (e *Entities) Set(id string, entity *models.Entity) { - e.set(id, entity) - e.emit(id, entity) -} diff --git a/internal/controller/modules.go b/internal/controller/modules.go deleted file mode 100644 index 80ffb53..0000000 --- a/internal/controller/modules.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package controller - -import ( - "fmt" - "sync" - "udap/internal/bond" - "udap/internal/models" - "udap/internal/store" -) - -type Modules struct { - PolyBuffer - Observable -} - -func LoadModules() (m *Modules) { - m = &Modules{} - m.data = sync.Map{} - m.raw = map[string]interface{}{} - m.Run() - m.FetchAll() - return m -} - -func (m *Modules) EmitAll() (err error) { - - for _, k := range m.Keys() { - find := m.Find(k) - m.emit(k, find) - } - - return nil -} - -func (m *Modules) Handle(event bond.Msg) (res interface{}, err error) { - switch o := event.Operation; o { - case "register": - return m.register(event) - case "enabled": - return m.enable(event) - default: - return nil, fmt.Errorf("invalid operation '%s'", o) - } -} - -func (m *Modules) enable(event bond.Msg) (res interface{}, err error) { - id := event.Id - err = m.Enabled(id, event.Payload == "true") - if err != nil { - return nil, err - } - return nil, nil -} - -func (m *Modules) register(event bond.Msg) (res interface{}, err error) { - module := event.Body.(*models.Module) - err = module.Emplace() - if err != nil { - return nil, err - } - m.Set(module.Id, module) - return nil, nil -} - -func (m *Modules) Register(module models.Module) (string, error) { - - err := module.Emplace() - if err != nil { - return "", err - } - - m.Set(module.Id, &module) - return module.Id, nil -} - -func (m *Modules) State(id string, state string) error { - find := m.Find(id) - find.State = state - - err := find.Update() - if err != nil { - return err - } - - m.Set(find.Id, find) - return nil -} - -func (m *Modules) Enabled(id string, enabled bool) error { - find := m.Find(id) - find.Enabled = enabled - - err := find.Update() - if err != nil { - return err - } - - m.Set(find.Id, find) - return nil -} - -func (m *Modules) FetchAll() { - var modules []*models.Module - store.DB.Model(&models.Module{}).Find(&modules) - for _, module := range modules { - m.set(module.Id, module) - } -} - -// Pull is the level at which this service needs to run -func (m *Modules) Pull() { - for _, k := range m.Keys() { - err := m.get(k) - if err != nil { - return - } - } -} - -func (m *Modules) Compile() (es []models.Module, err error) { - for _, k := range m.Keys() { - ea := m.get(k).(*models.Module) - es = append(es, *ea) - } - return es, err -} - -func (m *Modules) Find(name string) *models.Module { - get := m.get(name) - return get.(*models.Module) -} - -func (m *Modules) Set(id string, module *models.Module) { - m.set(id, module) - m.emit(id, module) -} diff --git a/internal/controller/network.go b/internal/controller/network.go deleted file mode 100644 index 7c3845c..0000000 --- a/internal/controller/network.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package controller - -import ( - "sync" - "udap/internal/bond" - "udap/internal/models" - "udap/internal/store" -) - -type Networks struct { - PolyBuffer - Observable -} - -func (d *Networks) Handle(event bond.Msg) (res interface{}, err error) { - switch event.Operation { - case "register": - return d.register(event) - } - return nil, nil -} - -func (d *Networks) Compile() (res []models.Network, err error) { - for _, s := range d.Keys() { - network := d.Find(s) - res = append(res, *network) - } - return res, nil -} - -func (d *Networks) Register(network *models.Network) (res *models.Network, err error) { - err = network.Emplace() - if err != nil { - return nil, err - } - d.Set(network.Id, network) - return nil, nil -} - -func (d *Networks) register(event bond.Msg) (res *models.Network, err error) { - network := event.Body.(*models.Network) - - return d.Register(network) -} - -func LoadNetworks() (m *Networks) { - m = &Networks{} - m.data = sync.Map{} - m.raw = map[string]interface{}{} - m.Run() - m.FetchAll() - return m -} - -func (d *Networks) EmitAll() (err error) { - - for _, k := range d.Keys() { - find := d.Find(k) - d.emit(k, find) - } - - return nil -} - -func (d *Networks) FetchAll() { - var networks []*models.Network - store.DB.Model(&models.Network{}).Find(&networks) - for _, network := range networks { - d.set(network.Id, network) - } -} - -// Pull is the level at which this service needs to run -func (d *Networks) Pull() { - for _, k := range d.Keys() { - err := d.get(k) - if err != nil { - return - } - } -} - -func (d *Networks) Find(name string) *models.Network { - return d.get(name).(*models.Network) -} - -func (d *Networks) Set(id string, entity *models.Network) { - d.set(id, entity) - d.emit(id, entity) -} diff --git a/internal/controller/user.go b/internal/controller/user.go deleted file mode 100644 index 71343cb..0000000 --- a/internal/controller/user.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package controller - -import ( - "sync" - "udap/internal/bond" - "udap/internal/log" - "udap/internal/models" - "udap/internal/store" -) - -type Users struct { - PolyBuffer - Observable -} - -// Handle routes websocket requests to the appropriate function -func (u *Users) Handle(event bond.Msg) (res interface{}, err error) { - switch event.Operation { - case "register": - return u.register(event) - case "authenticate": - return u.authenticate(event) - } - return nil, nil -} - -func (u *Users) EmitAll() (err error) { - - for _, k := range u.Keys() { - find := u.Find(k) - u.emit(k, find) - } - - return nil -} - -func (u *Users) FetchAll() []models.User { - var users []models.User - log.Log("Fetching") - err := store.DB.Table("users").Find(&users).Error - if err != nil { - return nil - } - return users -} - -func LoadUsers() (m *Users) { - m = &Users{} - m.data = sync.Map{} - m.raw = map[string]interface{}{} - m.Run() - m.FetchAll() - return m -} - -// register will create a new user from the provided body within 'payload' -func (u *Users) register(msg bond.Msg) (res interface{}, err error) { - user := models.User{} - err = user.Parse([]byte(msg.Payload)) - if err != nil { - return nil, err - } - err = user.Register(&user) - if err != nil { - return nil, err - } - return user, nil -} - -// authenticate will attempt to verify a user password and username combination -func (u *Users) authenticate(msg bond.Msg) (res interface{}, err error) { - user := models.User{} - err = user.Parse([]byte(msg.Payload)) - if err != nil { - return nil, err - } - dbUser, err := user.Authenticate(user) - if err != nil { - return nil, err - } - return dbUser, nil -} - -// Pull is the level at which this service needs to run -func (u *Users) Pull() { - for _, k := range u.Keys() { - err := u.get(k) - if err != nil { - return - } - } -} - -func (u *Users) Find(name string) *models.User { - return u.get(name).(*models.User) -} - -func (u *Users) Set(id string, entity *models.User) { - u.set(id, entity) - u.emit(id, entity) -} diff --git a/internal/controller/zone.go b/internal/controller/zone.go deleted file mode 100644 index c477479..0000000 --- a/internal/controller/zone.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package controller - -import ( - "encoding/json" - "sync" - "udap/internal/bond" - "udap/internal/log" - "udap/internal/models" - "udap/internal/store" -) - -type Zones struct { - PolyBuffer - Observable -} - -func (z *Zones) Handle(event bond.Msg) (res interface{}, err error) { - switch event.Operation { - case "compile": - return z.Compile() - case "create": - return z.create(event) - case "delete": - return z.delete(event) - case "restore": - return z.restore(event) - } - return nil, nil -} - -func (z *Zones) delete(msg bond.Msg) (res interface{}, err error) { - zone := z.Find(msg.Id) - - err = zone.Delete() - if err != nil { - return nil, err - } - z.Set(msg.Id, zone) - - return nil, err -} - -func (z *Zones) restore(msg bond.Msg) (res interface{}, err error) { - zone := z.Find(msg.Id) - - err = zone.Restore() - if err != nil { - return nil, err - } - z.Set(msg.Id, zone) - - return nil, err -} - -func (z *Zones) create(msg bond.Msg) (res interface{}, err error) { - zone := models.Zone{} - err = json.Unmarshal([]byte(msg.Payload), &zone) - if err != nil { - return nil, err - } - register, err := z.Register(zone) - if err != nil { - return nil, err - } - - return register, err -} - -func (z *Zones) Register(zone models.Zone) (res *models.Zone, err error) { - err = zone.Emplace() - if err != nil { - return nil, err - } - z.set(zone.Id, &zone) - z.emit(zone.Id, &zone) - return nil, nil -} - -func (z *Zones) Compile() (res []models.Zone, err error) { - for _, s := range z.Keys() { - zone := z.Find(s) - if zone == nil { - continue - } - - res = append(res, *zone) - } - return res, nil -} - -func (z *Zones) EmitAll() (err error) { - - for _, k := range z.Keys() { - find := z.Find(k) - z.emit(k, find) - } - - return nil -} - -func (z *Zones) FetchAll() { - var zones []models.Zone - err := store.DB.Model(&models.Zone{}).Preload("Entities").Find(&zones).Error - if err != nil { - log.Err(err) - return - } - for _, zone := range zones { - z.set(zone.Id, &zone) - z.emit(zone.Id, &zone) - } -} - -func (z *Zones) Find(id string) *models.Zone { - return z.get(id).(*models.Zone) -} - -func LoadZones() (m *Zones) { - m = &Zones{} - m.data = sync.Map{} - m.Run() - m.FetchAll() - return m -} - -func (z *Zones) Set(id string, zone *models.Zone) { - z.set(id, zone) - z.emit(zone.Id, &zone) -} - -func (z *Zones) Delete(id string) { - err := z.remove(id) - if err != nil { - return - } - -} diff --git a/internal/core/domain/attribute.go b/internal/core/domain/attribute.go index 2d67ec3..fd1d421 100644 --- a/internal/core/domain/attribute.go +++ b/internal/core/domain/attribute.go @@ -2,36 +2,73 @@ package domain -import "time" +import ( + "strconv" + "time" +) type Attribute struct { Persistent - Value string `json:"value"` - Updated time.Time `json:"updated"` - Request string `json:"request"` - Requested time.Time `json:"requested"` - Entity string `json:"entity"` - Key string `json:"key"` - Type string `json:"type"` - Order int `json:"order"` - Channel chan Attribute + Value string `json:"value"` + Updated time.Time `json:"updated"` + Request string `json:"request"` + Requested time.Time `json:"requested"` + Entity string `json:"entity"` + Key string `json:"key"` + Type string `json:"type"` + Order int `json:"order"` + Channel chan Attribute `gorm:"-"` // put FuncPut // get FuncGet } +func (a *Attribute) AsInt() int { + parsed, err := strconv.ParseInt(a.Value, 10, 64) + if err != nil { + return 0 + } + return int(parsed) +} + +func (a *Attribute) AsFloat() float64 { + parsed, err := strconv.ParseFloat(a.Value, 64) + if err != nil { + return 0.0 + } + return parsed +} + +func (a *Attribute) AsBool() bool { + parsed, err := strconv.ParseBool(a.Value) + if err != nil { + return false + } + return parsed +} + type AttributeRepository interface { FindAll() (*[]Attribute, error) FindAllByEntity(entity string) (*[]Attribute, error) FindById(id string) (*Attribute, error) FindByComposite(entity string, key string) (*Attribute, error) Create(*Attribute) error + Register(*Attribute) error FindOrCreate(*Attribute) error Update(*Attribute) error Delete(*Attribute) error } +type AttributeOperator interface { + Register(attribute *Attribute) error + Request(*Attribute, string) error + Set(*Attribute, string) error + Update(*Attribute, string, time.Time) error +} + type AttributeService interface { FindAll() (*[]Attribute, error) + EmitAll() error + Watch(chan<- Attribute) error FindAllByEntity(entity string) (*[]Attribute, error) FindById(id string) (*Attribute, error) Create(*Attribute) error diff --git a/internal/core/domain/attribute_test.go b/internal/core/domain/attribute_test.go new file mode 100644 index 0000000..c15f173 --- /dev/null +++ b/internal/core/domain/attribute_test.go @@ -0,0 +1,43 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +import ( + "testing" +) + +func TestAttribute_Sanity(t *testing.T) { + a := Attribute{ + Value: "applesauce", + } + if a.Value != "applesauce" { + t.Errorf("Value not set") + } +} + +func TestAttribute_AsInt(t *testing.T) { + a := Attribute{ + Value: "100", + } + if a.AsInt() != 100 { + t.Errorf("failed to convert string to int") + } +} + +func TestAttribute_AsFloat(t *testing.T) { + a := Attribute{ + Value: "123.456", + } + if a.AsFloat() != 123.456 { + t.Errorf("failed to convert string to float") + } +} + +func TestAttribute_AsBool(t *testing.T) { + a := Attribute{ + Value: "false", + } + if a.AsBool() != false { + t.Errorf("failed to convert string to bool") + } +} diff --git a/internal/core/domain/device.go b/internal/core/domain/device.go index 96ff0bb..83f9fde 100644 --- a/internal/core/domain/device.go +++ b/internal/core/domain/device.go @@ -27,6 +27,7 @@ type DeviceService interface { FindById(id string) (*Device, error) Create(*Device) error FindOrCreate(*Device) error + Register(*Device) error Update(*Device) error Delete(*Device) error } diff --git a/internal/core/domain/endpoint.go b/internal/core/domain/endpoint.go index f8c5c36..55c8890 100644 --- a/internal/core/domain/endpoint.go +++ b/internal/core/domain/endpoint.go @@ -2,6 +2,12 @@ package domain +import ( + "github.com/gorilla/websocket" + "math/rand" + "time" +) + type Endpoint struct { Persistent Name string `json:"name" gorm:"unique"` @@ -10,8 +16,30 @@ type Endpoint struct { Key string `json:"key"` } +func randomSequence() string { + template := "abcdefghijklmnopqrstuvwxyz" + var out string + rand.Seed(time.Now().Unix()) + for i := 0; i < 8; i++ { + r := rand.Intn(26) + u := template[r] + out += string(u) + } + return out +} + +func NewEndpoint(name string, variant string) *Endpoint { + return &Endpoint{ + Persistent: Persistent{}, + Name: name, + Type: variant, + Connected: false, + Key: randomSequence(), + } +} + type EndpointRepository interface { - FindAll() ([]*Endpoint, error) + FindAll() (*[]Endpoint, error) FindById(id string) (*Endpoint, error) FindByKey(key string) (*Endpoint, error) Create(*Endpoint) error @@ -20,13 +48,19 @@ type EndpointRepository interface { Delete(*Endpoint) error } +type EndpointOperator interface { + Enroll(*Endpoint, *websocket.Conn) error + Send(id string, operation string, payload any) error +} + type EndpointService interface { - FindAll() ([]*Endpoint, error) + FindAll() (*[]Endpoint, error) FindById(id string) (*Endpoint, error) FindByKey(key string) (*Endpoint, error) Create(*Endpoint) error - Enroll(key string) (*Endpoint, error) + Enroll(id string, conn *websocket.Conn) error + Send(id string, operation string, payload any) error Disconnect(key string) error FindOrCreate(*Endpoint) error diff --git a/internal/core/domain/entity.go b/internal/core/domain/entity.go index d28a20d..8341fde 100644 --- a/internal/core/domain/entity.go +++ b/internal/core/domain/entity.go @@ -22,6 +22,7 @@ type EntityRepository interface { FindById(id string) (*Entity, error) Create(*Entity) error FindOrCreate(*Entity) error + Register(*Entity) error Update(*Entity) error Delete(*Entity) error } @@ -32,7 +33,7 @@ type EntityService interface { Create(*Entity) error Config(id string, value string) error FindOrCreate(*Entity) error - Register(*Entity) (*Entity, error) + Register(*Entity) error Update(*Entity) error Delete(*Entity) error } diff --git a/internal/core/domain/module.go b/internal/core/domain/module.go index fb5a13e..4da2d43 100644 --- a/internal/core/domain/module.go +++ b/internal/core/domain/module.go @@ -26,9 +26,22 @@ type ModuleRepository interface { Delete(*Module) error } +type ModuleOperator interface { + Build(module *Module) error + Load(module *Module) error + Update(module *Module) error + Run(module *Module) error +} + type ModuleService interface { Discover() error Build(module *Module) error + Load(module *Module) error + Update(module *Module) error + Run(module *Module) error + UpdateAll() error + RunAll() error + LoadAll() error BuildAll() error FindAll() (*[]Module, error) FindByName(name string) (*Module, error) diff --git a/internal/core/domain/network.go b/internal/core/domain/network.go new file mode 100644 index 0000000..8e95834 --- /dev/null +++ b/internal/core/domain/network.go @@ -0,0 +1,34 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type Network struct { + Persistent + Name string `json:"name"` + Dns string `json:"dns"` + Router string `json:"index"` + Lease string `json:"lease"` + Mask string `json:"mask"` + Range string `json:"range"` +} + +type NetworkRepository interface { + FindAll() ([]*Network, error) + FindById(id string) (*Network, error) + FindByName(name string) (*Network, error) + Create(*Network) error + Register(*Network) error + FindOrCreate(*Network) error + Update(*Network) error + Delete(*Network) error +} + +type NetworkService interface { + FindAll() ([]*Network, error) + FindById(id string) (*Network, error) + Create(*Network) error + FindOrCreate(*Network) error + Register(*Network) error + Update(*Network) error + Delete(*Network) error +} diff --git a/internal/core/domain/zone.go b/internal/core/domain/zone.go new file mode 100644 index 0000000..9692ba8 --- /dev/null +++ b/internal/core/domain/zone.go @@ -0,0 +1,28 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type Zone struct { + Persistent + Name string `json:"name"` + Entities []Entity `json:"entities" gorm:"many2many:zone_entities;"` + User string `json:"user"` +} + +type ZoneRepository interface { + FindAll() ([]*Zone, error) + FindById(id string) (*Zone, error) + Create(*Zone) error + FindOrCreate(*Zone) error + Update(*Zone) error + Delete(*Zone) error +} + +type ZoneService interface { + FindAll() ([]*Zone, error) + FindById(id string) (*Zone, error) + Create(*Zone) error + FindOrCreate(*Zone) error + Update(*Zone) error + Delete(*Zone) error +} diff --git a/internal/core/migrate.go b/internal/core/migrate.go new file mode 100644 index 0000000..628861b --- /dev/null +++ b/internal/core/migrate.go @@ -0,0 +1,17 @@ +// Copyright (c) 2022 Braden Nicholson + +package core + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func MigrateModels(db *gorm.DB) error { + err := db.AutoMigrate(domain.Attribute{}, domain.Entity{}, domain.Module{}, domain.Device{}, domain.Endpoint{}, + domain.User{}, domain.Network{}, domain.Zone{}) + if err != nil { + return err + } + return nil +} diff --git a/internal/models/attribute.go b/internal/models/attribute.go deleted file mode 100644 index a3c17b5..0000000 --- a/internal/models/attribute.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package models - -import ( - "fmt" - "strconv" - "time" -) - -type FuncPut func(value string) error -type FuncGet func() (string, error) - -type Attribute struct { - Id string `json:"id"` - - Value string `json:"value"` - Updated time.Time `json:"updated"` - - Request string `json:"request"` - Requested time.Time `json:"requested"` - - Entity string `json:"entity"` - Key string `json:"key"` - Type string `json:"type"` - Order int `json:"order"` - put FuncPut - get FuncGet -} - -// Path Returns a unique identifier bound to an entity -func (a *Attribute) Path() string { - return fmt.Sprintf("%s.%s", a.Entity, a.Key) -} - -// SetValue overrides any existing value -func (a *Attribute) SetValue(val string) { - a.Request = val - // Overwrite the value - a.Value = val - // Update the timestamp for the current values time - a.Updated = time.Now() -} - -// UpdateValue attempts to write an update to the attribute -func (a *Attribute) UpdateValue(val string, stamp time.Time) error { - // If a request has been made in the last five seconds, and has been unresolved, ignore this update - if a.Requested.Before(stamp) && a.Request != val && time.Since(a.Requested) < 5*time.Second { - return fmt.Errorf("OVERWRITES REQUEST") - } - // Update the request value (since the request can be external) - a.Request = val - // Set the value - a.SetValue(val) - // Return no errors - return nil -} - -// SendRequest attempts to send a change to the attribute handler -func (a *Attribute) SendRequest(val string) error { - // If the attribute handler is not set, return an error - if a.put == nil { - return fmt.Errorf("attribute put function not connected") - } - // Register the request - a.Request = val - // Mark the request's time - a.Requested = time.Now() - // Attempt to send the value - err := a.put(val) - if err != nil { - return err - } - // Set the value - a.SetValue(val) - // Return no errors - return nil -} - -// FnPut registers the attributes set function -func (a *Attribute) FnPut(put FuncPut) { - a.put = put -} - -// FnGet registers the attributes get function -func (a *Attribute) FnGet(get FuncGet) { - a.get = get -} - -func (a *Attribute) AsInt() int { - parsed, err := strconv.ParseInt(a.Value, 10, 64) - if err != nil { - return 0 - } - return int(parsed) -} - -func (a *Attribute) AsFloat() float64 { - parsed, err := strconv.ParseFloat(a.Value, 64) - if err != nil { - return 0.0 - } - return parsed -} - -func (a *Attribute) AsBool() bool { - parsed, err := strconv.ParseBool(a.Value) - if err != nil { - return false - } - return parsed -} diff --git a/internal/models/attribute_test.go b/internal/models/attribute_test.go deleted file mode 100644 index d2beecc..0000000 --- a/internal/models/attribute_test.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package models - -import ( - "testing" - "time" -) - -func TestAttribute_Sanity(t *testing.T) { - a := Attribute{ - Value: "applesauce", - } - if a.Value != "applesauce" { - t.Errorf("Value not set") - } -} - -func TestAttribute_Path(t *testing.T) { - a := Attribute{ - Entity: "abc123", - Key: "cba321", - } - if a.Path() != "abc123.cba321" { - t.Errorf("Attribute path is malformed") - } -} - -func TestAttribute_FnGet(t *testing.T) { - a := Attribute{ - Entity: "abc123", - Key: "cba321", - } - - a.FnGet(func() (string, error) { - return "xyz123", nil - }) - get, err := a.get() - if err != nil { - t.Error(err) - } - if get != "xyz123" { - t.Error("function did not change value") - } -} - -func TestAttribute_FnPut(t *testing.T) { - a := Attribute{ - Entity: "abc123", - Key: "cba321", - } - - a.FnPut(func(value string) error { - if value != "xyz123" { - t.Error("function did not change value") - } - return nil - }) - err := a.put("xyz123") - if err != nil { - t.Error(err) - } - -} - -func TestAttribute_UpdateValue(t *testing.T) { - a := Attribute{ - Request: "applesauce", - Requested: time.Now(), - } - err := a.UpdateValue("orangejuice", time.Now()) - if err == nil { - t.Errorf("value should not be updated") - } - if a.Request != "applesauce" { - t.Errorf("value should not be overwritten") - } -} - -func TestAttribute_SendRequest(t *testing.T) { - a := Attribute{ - Request: "applesauce", - Requested: time.Now(), - } - err := a.SendRequest("orangejuice") - if err == nil { - t.Errorf("functions not set, should return error") - } -} - -func TestAttribute_AsInt(t *testing.T) { - a := Attribute{ - Value: "100", - } - if a.AsInt() != 100 { - t.Errorf("failed to convert string to int") - } -} - -func TestAttribute_AsFloat(t *testing.T) { - a := Attribute{ - Value: "123.456", - } - if a.AsFloat() != 123.456 { - t.Errorf("failed to convert string to float") - } -} - -func TestAttribute_AsBool(t *testing.T) { - a := Attribute{ - Value: "false", - } - if a.AsBool() != false { - t.Errorf("failed to convert string to bool") - } -} diff --git a/internal/models/device.go b/internal/models/device.go deleted file mode 100644 index 6fe1746..0000000 --- a/internal/models/device.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package models - -import ( - "time" - "udap/internal/log" - "udap/internal/store" -) - -type Device struct { - store.Persistent - NetworkId string `json:"networkId" gorm:"-"` - EntityId string `json:"entityId" gorm:"-"` - Name string `json:"name"` - Hostname string `json:"hostname"` - Mac string `json:"mac"` - Ipv4 string `json:"ipv4"` - Ipv6 string `json:"ipv6"` -} - -func (d *Device) Emplace() (err error) { - d.UpdatedAt = time.Now() - err = store.DB.Model(&Device{}).Where("mac = ? OR id = ?", d.Mac, d.Id).FirstOrCreate(d).Error - if err != nil { - return err - } - return nil -} - -func (d *Device) FetchAll() []Device { - var devices []Device - log.Log("Fetching") - err := store.DB.Table("devices").Find(&devices).Error - if err != nil { - return nil - } - return devices -} - -func (d *Device) Update() error { - err := store.DB.Where("mac = ?", d.Mac).Save(&d).Error - return err -} - -func NewDevice() Device { - device := Device{} - return device -} diff --git a/internal/models/endpoint.go b/internal/models/endpoint.go deleted file mode 100644 index 2f06df8..0000000 --- a/internal/models/endpoint.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package models - -import ( - "encoding/json" - "fmt" - "github.com/gorilla/websocket" - "gorm.io/gorm" - "math/rand" - "time" - "udap/internal/log" - "udap/internal/store" -) - -type Connection struct { - WS *websocket.Conn - active *bool - edit chan interface{} - done chan bool -} - -func (c *Connection) Active() bool { - return *c.active -} - -func (c *Connection) Send(body interface{}) { - if c.Active() { - c.edit <- body - } -} - -func NewConnection(ws *websocket.Conn) *Connection { - ch := make(chan interface{}) - d := make(chan bool) - a := true - c := &Connection{ - WS: ws, - edit: ch, - done: d, - active: &a, - } - - return c -} - -func (c *Connection) Close() { - close(c.edit) - a := false - c.active = &a -} - -func (c *Connection) Watch() { - for a := range c.edit { - if c.WS == nil { - return - } - err := c.WS.WriteJSON(a) - if err != nil { - continue - } - } -} - -// Endpoint represents a client device connected to the UDAP network -type Endpoint struct { - store.Persistent - - Name string `json:"name" gorm:"unique"` - - Type string `json:"type" gorm:"default:'terminal'"` - - Frequency int `json:"frequency" gorm:"default:3000"` - - Connected bool `json:"connected"` - - Key string `json:"key"` - - registered bool - Connection *Connection `json:"-" gorm:"-"` - enrolledSince time.Time `gorm:"-"` -} - -func (e *Endpoint) Compile() (a map[string]interface{}) { - marshal, err := json.Marshal(e) - if err != nil { - return nil - } - err = json.Unmarshal(marshal, &a) - if err != nil { - return nil - } - return a -} - -func (e *Endpoint) Enroll(ws *websocket.Conn) error { - err := store.DB.Model(&Endpoint{}).FirstOrCreate(&e).Error - if err != nil { - return err - } - ws.SetCloseHandler(e.closeHandler) - e.Connection = NewConnection(ws) - e.Connected = true - e.registered = true - e.enrolledSince = time.Now() - log.Log("Endpoint '%s' enrolled (%s)", e.Name, ws.LocalAddr()) - - return nil -} - -func NewEndpoint(name string) Endpoint { - endpoint := Endpoint{} - endpoint.Name = name - endpoint.Type = "terminal" - return endpoint -} - -func (e *Endpoint) closeHandler(code int, text string) error { - if e.Enrolled() { - e.Unenroll() - } - - return nil -} - -func (e *Endpoint) Enrolled() bool { - return e.registered -} - -// BeforeCreate is a hook function from gorm, called when an endpoint is inserted -func (e *Endpoint) BeforeCreate(_ *gorm.DB) error { - e.Key = randomSequence() - return nil -} - -// randomSequence generates a random id for use as a key -func randomSequence() string { - template := "abcdefghijklmnopqrstuvwxyz" - var out string - rand.Seed(time.Now().Unix()) - for i := 0; i < 8; i++ { - r := rand.Intn(26) - u := template[r] - out += string(u) - } - return out -} - -// Fetch populates the struct from the database -func (e *Endpoint) Fetch() error { - if e.Id == "" { - return fmt.Errorf("invalid id") - } - err := store.DB.Where("name = ? OR id = ?", e.Name, e.Id).First(&e).Error - if err != nil { - return err - } - - return nil -} - -func (e *Endpoint) Unenroll() { - e.registered = false - e.Connected = false - e.Connection.Close() - log.Log("Endpoint '%s' unenrolled (%s)", e.Name, time.Since(e.enrolledSince).String()) -} diff --git a/internal/models/entity.go b/internal/models/entity.go deleted file mode 100644 index d6b6c92..0000000 --- a/internal/models/entity.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package models - -import ( - "fmt" - "strings" - "udap/internal/store" -) - -type Entity struct { - store.Persistent - Name string `gorm:"unique" json:"name"` // Given name from module - Alias string `json:"alias"` // Name from users - Type string `json:"type"` // Type of entity {Light, Sensor, Etc} - Module string `json:"module"` // Parent Module name - Locked bool `json:"locked"` // Is the Entity state locked? - Config string `json:"config"` - - Position string `json:"position" gorm:"default:'{}'"` - - Icon string `json:"icon" gorm:"default:'􀛮'"` // The icon to represent this entity - Frequency int `json:"frequency" gorm:"default:3000"` - - Neural string `json:"neural" gorm:"default:'inactive'"` // Parent Module name - Predicted string `gorm:"-" json:"predicted"` // scalar -} - -func (e *Entity) Unlock() error { - if !e.Locked { - return fmt.Errorf("this entity is not locked") - } - e.Locked = false - err := e.update() - if err != nil { - return err - } - return nil -} - -func (e *Entity) Lock() error { - if e.Locked { - return fmt.Errorf("this entity is already locked") - } - e.Locked = true - err := e.update() - if err != nil { - return err - } - return nil -} - -func (e *Entity) ChangeConfig(value string) error { - e.Config = value - err := e.update() - if err != nil { - return err - } - return nil -} - -func (e *Entity) ChangeNeural(value string) error { - e.Neural = value - err := e.update() - if err != nil { - return err - } - return nil -} - -func (e *Entity) ChangeIcon(icon string) error { - e.Icon = icon - err := e.update() - if err != nil { - return err - } - return nil -} - -func (e *Entity) Rename(name string) error { - if e.Alias == name { - return fmt.Errorf("alias has not been changed") - } - var cnt int64 - store.DB.Where("alias = ?", name).Count(&cnt) - if cnt >= 1 { - return fmt.Errorf("alias is already in use") - } - err := e.update() - if err != nil { - return err - } - return nil -} - -func (e *Entity) Suggest(state string) error { - e.Predicted = state - err := e.update() - if err != nil { - return err - } - return nil -} - -// Find attempts to locate -func (e *Entity) Find() error { - err := store.DB.Where("name = ? AND module = ?", e.Name, e.Module).First(&e).Error - return err -} - -// Path attempts to locate -func (e *Entity) Path() string { - return strings.ToLower(fmt.Sprintf("%s.%s", e.Module, e.Name)) -} - -func (e *Entity) Emplace() error { - if e.Id == "" { - err := store.DB.Model(&Entity{}).Where("name = ? AND module = ?", e.Name, e.Module).FirstOrCreate(e).Error - if err != nil { - return err - } - } else { - err := store.DB.Model(&Entity{}).Where("id = ?", e.Name).First(e).Error - if err != nil { - return err - } - } - err := store.DB.Model(&Entity{}).Where("id = ?", e.Id).Save(e).Error - if err != nil { - return err - } - - return nil -} - -func (e *Entity) delete() error { - err := store.DB.Where("name = ? AND module = ?", e.Name, e.Module).Delete(&e).Error - return err -} - -func (e *Entity) update() error { - err := store.DB.Where("id = ?", e.Id).Save(e).Error - if err != nil { - return err - } - - return err -} - -func NewMediaEntity(name string, module string) *Entity { - e := Entity{ - Name: name, - Type: "media", - Module: module, - } - return &e -} - -func NewSpectrum(name string, module string) *Entity { - - e := Entity{ - Name: name, - Type: "spectrum", - Module: module, - } - return &e -} - -func NewDimmer(name string, module string) *Entity { - - e := Entity{ - Name: name, - Type: "dimmer", - Module: module, - } - return &e -} - -func NewSwitch(name string, module string) *Entity { - - e := Entity{ - Name: name, - Type: "switch", - Module: module, - } - return &e -} diff --git a/internal/models/log.go b/internal/models/log.go deleted file mode 100644 index ccecffb..0000000 --- a/internal/models/log.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package models - -import ( - "udap/internal/store" -) - -type Log struct { - store.Persistent - EntityId string `json:"entityId"` - Power string `json:"power"` - Mode string `json:"mode"` - Level int `json:"level"` - CCT int `json:"cct"` -} diff --git a/internal/models/migrate.go b/internal/models/migrate.go deleted file mode 100644 index 645acd9..0000000 --- a/internal/models/migrate.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package models - -import "udap/internal/store" - -func MigrateModels() error { - err := store.DB.AutoMigrate(Log{}, Endpoint{}, Entity{}, Module{}, Device{}, Network{}, Zone{}, User{}) - if err != nil { - return err - } - return nil -} diff --git a/internal/models/module.go b/internal/models/module.go deleted file mode 100644 index a3367b8..0000000 --- a/internal/models/module.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package models - -import ( - "fmt" - "gorm.io/gorm" - "time" - "udap/internal/store" -) - -type Module struct { - store.Persistent - Name string `json:"name"` - Path string `json:"path"` - Type string `json:"type"` - Description string `json:"description"` - Version string `json:"version"` - Author string `json:"author"` - Channel chan Module `json:"-" gorm:"-"` - State string `json:"state"` - Enabled bool `json:"enabled" gorm:"default:true"` - Recover int `json:"recover"` -} - -// create inserts the current module into the database -func (m *Module) Register() error { - // Attempt to create a new module - err := store.DB.Create(m).Error - // Report internal errors for later diagnostic - if err != nil { - return fmt.Errorf("failed to create module") - } - // Return no errors - return nil -} - -// Hooks - -func (m *Module) BeforeCreate(_ *gorm.DB) error { - - return nil -} - -func (m *Module) AfterFind(_ *gorm.DB) error { - - return nil -} - -// Emplace gets a module from its path -func (m *Module) Update() (err error) { - m.UpdatedAt = time.Now() - err = store.DB.Model(&Module{}).Where("id = ?", m.Id).Save(&m).Error - if err != nil { - return err - } - if m.Channel != nil { - m.Channel <- *m - } - - return nil -} - -// Emplace gets a module from its path -func (m *Module) Emplace() (err error) { - m.UpdatedAt = time.Now() - err = store.DB.Model(&Module{}).Where("path = ?", m.Path).FirstOrCreate(&m).Error - if err != nil { - return err - } - if m.Channel != nil { - m.Channel <- *m - } - return nil -} diff --git a/internal/models/network.go b/internal/models/network.go deleted file mode 100644 index 32eb713..0000000 --- a/internal/models/network.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package models - -import ( - "time" - "udap/internal/store" -) - -type Network struct { - store.Persistent - Name string `json:"name"` - Dns string `json:"dns"` - Router string `json:"index"` - Lease string `json:"lease"` - Mask string `json:"mask"` - Range string `json:"range"` -} - -// Emplace gets a module from its path -func (n *Network) Emplace() (err error) { - n.UpdatedAt = time.Now() - err = store.DB.Model(&Network{}).Where("name = ?", n.Name).FirstOrCreate(&n).Error - if err != nil { - return err - } - return nil -} - -func (n *Network) FetchAll() []Network { - var networks []Network - store.DB.Model(&Network{}).Find(&networks) - return networks -} diff --git a/internal/models/remote.go b/internal/models/remote.go deleted file mode 100644 index bc3b14e..0000000 --- a/internal/models/remote.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package models - -import ( - "encoding/json" - "github.com/gorilla/websocket" - "sync" - "udap/internal/log" -) - -type Remote struct { - conn *websocket.Conn - rw *sync.Mutex -} - -func NewRemote(c *websocket.Conn) Remote { - r := Remote{ - rw: &sync.Mutex{}, - } - r.rw.Lock() - r.conn = c - log.Log("Get socket opened: %s", c.RemoteAddr()) - r.conn.SetCloseHandler(r.closeHandler) - r.rw.Unlock() - return r -} - -func (r *Remote) closeHandler(code int, text string) error { - if text == "" { - text = "[empty]" - } - log.Log("Get socket closed: %s (%d)", text, code) - return nil -} - -func (r *Remote) Send(body json.RawMessage) error { - r.rw.Lock() - err := r.conn.WriteJSON(body) - if err != nil { - log.Err(err) - } - r.rw.Unlock() - return err -} - -func (r *Remote) Close() error { - - err := r.conn.Close() - if err != nil { - log.Err(err) - } - return err -} - -func (r *Remote) Read() (body json.RawMessage, err error) { - r.rw.Lock() - err = r.conn.ReadJSON(body) - if err != nil { - log.Err(err) - } - r.rw.Unlock() - return body, err -} diff --git a/internal/models/user.go b/internal/models/user.go deleted file mode 100644 index 7bee7cb..0000000 --- a/internal/models/user.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package models - -import ( - "encoding/json" - "fmt" - "golang.org/x/crypto/bcrypt" - _ "golang.org/x/crypto/bcrypt" - "udap/internal/store" -) - -type User struct { - store.Persistent - Username string `json:"username"` - First string `json:"first"` - Middle string `json:"middle"` - Last string `json:"last"` - Type string `json:"type"` - Password string `json:"password"` -} - -func (u *User) Parse(data []byte) error { - if !json.Valid(data) { - return fmt.Errorf("failed to parse invalid json for type 'user'") - } - err := json.Unmarshal(data, u) - if err != nil { - return fmt.Errorf("failed to parse json for type 'user': %s", err.Error()) - } - return nil -} - -func HashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) - return string(bytes), err -} - -func CheckPasswordHash(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} - -func (u *User) Register(user *User) error { - password, err := HashPassword(user.Password) - if err != nil { - return err - } - user.Password = password - err = store.DB.Create(user).Error - if err != nil { - return err - } - return nil -} - -func (u *User) FindById(id string) (user User, err error) { - err = store.DB.Where("id = ?", id).First(&user).Error - return -} - -func (u *User) Authenticate(user User) (User, error) { - pass := user.Password - err := store.DB.Where("username = ?", user.Username).First(&user).Error - if err != nil { - return User{}, nil - } - hash := CheckPasswordHash(pass, user.Password) - if !hash { - return User{}, fmt.Errorf("invalid password") - } - return user, nil -} diff --git a/internal/models/zone.go b/internal/models/zone.go deleted file mode 100644 index 3eaa3b3..0000000 --- a/internal/models/zone.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package models - -import ( - "time" - "udap/internal/store" -) - -type Zone struct { - store.Persistent - Name string `json:"name"` - Entities []Entity `json:"entities" gorm:"many2many:zone_entities;"` - User string `json:"user"` -} - -// Emplace will Find or Create a zone based on its id. -func (z *Zone) Emplace() (err error) { - z.UpdatedAt = time.Now() - err = store.DB.Model(&Zone{}).FirstOrCreate(z).Error - if err != nil { - return err - } - return nil -} - -func (z *Zone) FetchAll() (err error, zones []Zone) { - if err = store.DB.Table("zones").Preload("Entities").Find(&zones).Error; err != nil { - return err, nil - } - return nil, zones -} - -func (z *Zone) Update() error { - return store.DB.Where("id = ?", z.Id).Save(&z).Error -} - -func (z *Zone) Restore() error { - z.Deleted = false - return z.Update() -} - -// Delete marks the zone as deleted and discontinues its function -func (z *Zone) Delete() error { - z.Deleted = true - return z.Update() -} diff --git a/internal/modules/attribute/attribute.go b/internal/modules/attribute/attribute.go index fb4f783..995346c 100644 --- a/internal/modules/attribute/attribute.go +++ b/internal/modules/attribute/attribute.go @@ -9,6 +9,7 @@ import ( func New(db *gorm.DB) domain.AttributeService { repo := NewRepository(db) - service := NewService(repo) + operators := NewOperator() + service := NewService(repo, operators) return service } diff --git a/internal/modules/attribute/operator.go b/internal/modules/attribute/operator.go new file mode 100644 index 0000000..86c228e --- /dev/null +++ b/internal/modules/attribute/operator.go @@ -0,0 +1,67 @@ +// Copyright (c) 2022 Braden Nicholson + +package attribute + +import ( + "fmt" + "time" + "udap/internal/core/domain" +) + +type attributeOperator struct { + hooks map[string]chan domain.Attribute +} + +func NewOperator() domain.AttributeOperator { + return &attributeOperator{ + hooks: map[string]chan domain.Attribute{}, + } +} + +func (a attributeOperator) Register(attribute *domain.Attribute) error { + a.hooks[attribute.Id] = attribute.Channel + return nil +} + +func (a attributeOperator) Request(attribute *domain.Attribute, s string) error { + err := a.Set(attribute, s) + if err != nil { + return err + } + return nil +} + +func (a attributeOperator) Set(attribute *domain.Attribute, s string) error { + // If the attribute handler is not set, return an error + channel := a.hooks[attribute.Id] + + attribute.Request = s + + attribute.Value = s + + attribute.Requested = time.Now() + + if channel == nil { + return fmt.Errorf("channel is not open") + } + + channel <- *attribute + + return nil +} + +func (a attributeOperator) Update(attribute *domain.Attribute, val string, stamp time.Time) error { + // If a request has been made in the last five seconds, and has been unresolved, ignore this update + if attribute.Requested.Before(stamp) && attribute.Request != val && time.Since(attribute.Requested) < 5*time.Second { + return fmt.Errorf("OVERWRITES REQUEST") + } + // Update the request value (since the request can be external) + attribute.Request = val + // Set the value + err := a.Set(attribute, val) + if err != nil { + return err + } + // Return no errors + return nil +} diff --git a/internal/modules/attribute/repository.go b/internal/modules/attribute/repository.go index 020450a..3f177f3 100644 --- a/internal/modules/attribute/repository.go +++ b/internal/modules/attribute/repository.go @@ -17,12 +17,33 @@ func NewRepository(db *gorm.DB) domain.AttributeRepository { } } +func (u attributeRepo) Register(attribute *domain.Attribute) error { + if attribute.Id == "" { + err := u.db.Model(&domain.Attribute{}).Where("entity = ? AND key = ?", + attribute.Entity, attribute.Key).FirstOrCreate(attribute).Error + if err != nil { + return err + } + } else { + err := u.db.Model(&domain.Attribute{}).Where("id = ?", attribute.Id).First(attribute).Error + if err != nil { + return err + } + } + err := u.db.Model(&domain.Attribute{}).Where("id = ?", attribute.Id).Save(attribute).Error + if err != nil { + return err + } + return nil +} + func (u attributeRepo) FindByComposite(entity string, key string) (*domain.Attribute, error) { - var target *domain.Attribute - if err := u.db.Where("entity = ? && key = ?", entity, key).First(target).Error; err != nil { + var target domain.Attribute + if err := u.db.Model(&domain.Attribute{}).Where("entity = ? AND key = ?", entity, + key).First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u attributeRepo) FindAllByEntity(entity string) (*[]domain.Attribute, error) { @@ -34,19 +55,19 @@ func (u attributeRepo) FindAllByEntity(entity string) (*[]domain.Attribute, erro } func (u attributeRepo) FindAll() (*[]domain.Attribute, error) { - var target *[]domain.Attribute - if err := u.db.First(target).Error; err != nil { + var target []domain.Attribute + if err := u.db.First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u attributeRepo) FindById(id string) (*domain.Attribute, error) { - var target *domain.Attribute - if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + var target domain.Attribute + if err := u.db.Where("id = ?", id).First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u attributeRepo) Create(attribute *domain.Attribute) error { diff --git a/internal/modules/attribute/service.go b/internal/modules/attribute/service.go index 4d737c6..21ba404 100644 --- a/internal/modules/attribute/service.go +++ b/internal/modules/attribute/service.go @@ -3,51 +3,112 @@ package attribute import ( - "fmt" "time" "udap/internal/core/domain" ) type attributeService struct { repository domain.AttributeRepository - hooks map[string]chan domain.Attribute + operator domain.AttributeOperator + channel chan<- domain.Attribute +} + +func (u *attributeService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + attributes := *all + for _, attribute := range attributes { + err = u.push(&attribute) + if err != nil { + return err + } + } + return nil +} + +func (u *attributeService) push(attribute *domain.Attribute) error { + u.channel <- *attribute + return nil +} + +func (u *attributeService) Watch(channel chan<- domain.Attribute) error { + u.channel = channel + return nil } func (u attributeService) FindAllByEntity(entity string) (*[]domain.Attribute, error) { return u.repository.FindAllByEntity(entity) } -func NewService(repository domain.AttributeRepository) domain.AttributeService { - return attributeService{ +func NewService(repository domain.AttributeRepository, operator domain.AttributeOperator) domain.AttributeService { + return &attributeService{ repository: repository, - hooks: map[string]chan domain.Attribute{}, + operator: operator, + channel: nil, } } func (u attributeService) Register(attribute *domain.Attribute) error { - attribute, err := u.repository.FindByComposite(attribute.Entity, attribute.Key) + err := u.repository.Register(attribute) if err != nil { return err } - if u.hooks[attribute.Id] != nil { - return fmt.Errorf("attribute already registered") + err = u.operator.Register(attribute) + if err != nil { + return err } - u.hooks[attribute.Id] = attribute.Channel - attribute.Channel = nil + return nil } func (u attributeService) Request(entity string, key string, value string) error { + e, err := u.repository.FindByComposite(entity, key) + if err != nil { + return err + } + err = u.operator.Request(e, value) + if err != nil { + return err + } + err = u.push(e) + if err != nil { + return err + } return nil } func (u attributeService) Set(entity string, key string, value string) error { + e, err := u.repository.FindByComposite(entity, key) + if err != nil { + return err + } + err = u.operator.Set(e, value) + if err != nil { + return err + } + err = u.push(e) + if err != nil { + return err + } return nil } func (u attributeService) Update(entity string, key string, value string, stamp time.Time) error { - // TODO implement me - panic("implement me") + e, err := u.repository.FindByComposite(entity, key) + if err != nil { + return err + } + err = u.operator.Update(e, value, stamp) + if err != nil { + return err + } + err = u.push(e) + if err != nil { + return err + } + return nil } // Repository Mapping diff --git a/internal/modules/device/device.go b/internal/modules/device/device.go new file mode 100644 index 0000000..59a3207 --- /dev/null +++ b/internal/modules/device/device.go @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Braden Nicholson + +package device + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func New(db *gorm.DB) domain.DeviceService { + repo := NewRepository(db) + service := NewService(repo) + return service +} diff --git a/internal/modules/device/repository.go b/internal/modules/device/repository.go new file mode 100644 index 0000000..e7bfd76 --- /dev/null +++ b/internal/modules/device/repository.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Braden Nicholson + +package device + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type deviceRepo struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) domain.DeviceRepository { + return &deviceRepo{ + db: db, + } +} + +func (u deviceRepo) FindAll() ([]*domain.Device, error) { + var target []*domain.Device + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u deviceRepo) FindById(id string) (*domain.Device, error) { + var target *domain.Device + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u deviceRepo) Create(device *domain.Device) error { + if err := u.db.Create(device).Error; err != nil { + return err + } + return nil +} + +func (u deviceRepo) FindOrCreate(device *domain.Device) error { + if err := u.db.FirstOrCreate(device).Error; err != nil { + return err + } + return nil +} + +func (u deviceRepo) Update(device *domain.Device) error { + if err := u.db.Save(device).Error; err != nil { + return err + } + return nil +} + +func (u deviceRepo) Delete(device *domain.Device) error { + if err := u.db.Delete(device).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/device/service.go b/internal/modules/device/service.go new file mode 100644 index 0000000..7b831eb --- /dev/null +++ b/internal/modules/device/service.go @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Braden Nicholson + +package device + +import ( + "udap/internal/core/domain" +) + +type deviceService struct { + repository domain.DeviceRepository +} + +func (u deviceService) Register(device *domain.Device) error { + return nil +} + +func NewService(repository domain.DeviceRepository) domain.DeviceService { + return deviceService{repository: repository} +} + +// Repository Mapping + +func (u deviceService) FindAll() ([]*domain.Device, error) { + return u.repository.FindAll() +} + +func (u deviceService) FindById(id string) (*domain.Device, error) { + return u.repository.FindById(id) +} + +func (u deviceService) Create(device *domain.Device) error { + return u.repository.Create(device) +} + +func (u deviceService) FindOrCreate(device *domain.Device) error { + return u.repository.FindOrCreate(device) +} + +func (u deviceService) Update(device *domain.Device) error { + return u.repository.Update(device) +} + +func (u deviceService) Delete(device *domain.Device) error { + return u.repository.Delete(device) +} diff --git a/internal/modules/endpoint/endpoint.go b/internal/modules/endpoint/endpoint.go index 881c927..c096754 100644 --- a/internal/modules/endpoint/endpoint.go +++ b/internal/modules/endpoint/endpoint.go @@ -9,6 +9,7 @@ import ( func New(db *gorm.DB) domain.EndpointService { repo := NewRepository(db) - service := NewService(repo) + operator := NewOperator() + service := NewService(repo, operator) return service } diff --git a/internal/modules/endpoint/operator.go b/internal/modules/endpoint/operator.go new file mode 100644 index 0000000..d1b753d --- /dev/null +++ b/internal/modules/endpoint/operator.go @@ -0,0 +1,69 @@ +// Copyright (c) 2022 Braden Nicholson + +package endpoint + +import ( + "github.com/gorilla/websocket" + "udap/internal/core/domain" + "udap/internal/log" +) + +type endpointOperator struct { + connections map[string]*websocket.Conn +} + +func (m *endpointOperator) Send(id string, operation string, payload any) error { + if m.connections[id] == nil { + return nil + } + err := m.connections[id].WriteJSON(Response{ + Status: "success", + Operation: operation, + Body: payload, + }) + if err != nil { + return err + } + return nil +} + +type Metadata struct { + System System `json:"system"` +} + +type Response struct { + Id string `json:"id"` + Status string `json:"status"` + Operation string `json:"operation"` + Body any `json:"body"` +} + +func (m *endpointOperator) Enroll(endpoint *domain.Endpoint, conn *websocket.Conn) error { + m.connections[endpoint.Id] = conn + m.connections[endpoint.Id].SetCloseHandler(func(code int, text string) error { + log.Event("Endpoint '%s' disconnected.", endpoint.Name) + return nil + }) + info, err := systemInfo() + if err != nil { + return err + } + err = conn.WriteJSON(Response{ + Id: "", + Status: "success", + Operation: "metadata", + Body: Metadata{System: info}, + }) + if err != nil { + return err + } + + log.Event("Endpoint '%s' connected.", endpoint.Name) + return nil +} + +func NewOperator() domain.EndpointOperator { + return &endpointOperator{ + connections: map[string]*websocket.Conn{}, + } +} diff --git a/internal/modules/endpoint/repository.go b/internal/modules/endpoint/repository.go index 42cdc73..3ba4ca5 100644 --- a/internal/modules/endpoint/repository.go +++ b/internal/modules/endpoint/repository.go @@ -18,27 +18,27 @@ func NewRepository(db *gorm.DB) domain.EndpointRepository { } func (u endpointRepo) FindByKey(key string) (*domain.Endpoint, error) { - var target *domain.Endpoint - if err := u.db.Where("key = ?", key).First(target).Error; err != nil { + var target domain.Endpoint + if err := u.db.Where("key = ?", key).First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } -func (u endpointRepo) FindAll() ([]*domain.Endpoint, error) { - var target []*domain.Endpoint - if err := u.db.First(target).Error; err != nil { +func (u endpointRepo) FindAll() (*[]domain.Endpoint, error) { + var target []domain.Endpoint + if err := u.db.First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u endpointRepo) FindById(id string) (*domain.Endpoint, error) { - var target *domain.Endpoint - if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + var target domain.Endpoint + if err := u.db.Model(&domain.Endpoint{}).Where("id = ?", id).First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u endpointRepo) Create(endpoint *domain.Endpoint) error { diff --git a/internal/modules/endpoint/service.go b/internal/modules/endpoint/service.go index cdf666a..4b02425 100644 --- a/internal/modules/endpoint/service.go +++ b/internal/modules/endpoint/service.go @@ -3,20 +3,37 @@ package endpoint import ( + "github.com/gorilla/websocket" "udap/internal/core/domain" ) type endpointService struct { repository domain.EndpointRepository + operator domain.EndpointOperator +} + +func (u endpointService) Send(id string, operation string, payload any) error { + err := u.operator.Send(id, operation, payload) + if err != nil { + return err + } + return nil } func (u endpointService) FindByKey(key string) (*domain.Endpoint, error) { return u.repository.FindByKey(key) } -func (u endpointService) Enroll(key string) (*domain.Endpoint, error) { - // TODO implement me - panic("implement me") +func (u endpointService) Enroll(id string, conn *websocket.Conn) error { + endpoint, err := u.FindById(id) + if err != nil { + return err + } + err = u.operator.Enroll(endpoint, conn) + if err != nil { + return err + } + return nil } func (u endpointService) Disconnect(key string) error { @@ -24,13 +41,13 @@ func (u endpointService) Disconnect(key string) error { panic("implement me") } -func NewService(repository domain.EndpointRepository) domain.EndpointService { - return endpointService{repository: repository} +func NewService(repository domain.EndpointRepository, operator domain.EndpointOperator) domain.EndpointService { + return endpointService{repository: repository, operator: operator} } // Repository Mapping -func (u endpointService) FindAll() ([]*domain.Endpoint, error) { +func (u endpointService) FindAll() (*[]domain.Endpoint, error) { return u.repository.FindAll() } diff --git a/internal/server/system.go b/internal/modules/endpoint/system.go similarity index 94% rename from internal/server/system.go rename to internal/modules/endpoint/system.go index 0e31b6f..d8345de 100644 --- a/internal/server/system.go +++ b/internal/modules/endpoint/system.go @@ -1,6 +1,6 @@ // Copyright (c) 2022 Braden Nicholson -package server +package endpoint import ( "fmt" @@ -11,7 +11,12 @@ import ( func GetOutboundIP() (net.IP, error) { conn, err := net.Dial("udp", "8.8.8.8:80") - defer conn.Close() + defer func(conn net.Conn) { + err = conn.Close() + if err != nil { + + } + }(conn) if err != nil { return nil, err } diff --git a/internal/modules/entity/repository.go b/internal/modules/entity/repository.go index 070c86d..7f4131d 100644 --- a/internal/modules/entity/repository.go +++ b/internal/modules/entity/repository.go @@ -11,6 +11,25 @@ type entityRepo struct { db *gorm.DB } +func (u entityRepo) Register(e *domain.Entity) error { + if e.Id == "" { + err := u.db.Model(&domain.Entity{}).Where("name = ? AND module = ?", e.Name, e.Module).FirstOrCreate(e).Error + if err != nil { + return err + } + } else { + err := u.db.Model(&domain.Entity{}).Where("name = ?", e.Name).First(e).Error + if err != nil { + return err + } + } + err := u.db.Model(&domain.Entity{}).Where("name = ?", e.Name).Save(e).Error + if err != nil { + return err + } + return nil +} + func NewRepository(db *gorm.DB) domain.EntityRepository { return &entityRepo{ db: db, @@ -18,11 +37,11 @@ func NewRepository(db *gorm.DB) domain.EntityRepository { } func (u entityRepo) FindAll() (*[]domain.Entity, error) { - var target *[]domain.Entity - if err := u.db.First(target).Error; err != nil { + var target []domain.Entity + if err := u.db.Find(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u entityRepo) FindById(id string) (*domain.Entity, error) { diff --git a/internal/modules/entity/service.go b/internal/modules/entity/service.go index 890db61..af9fb79 100644 --- a/internal/modules/entity/service.go +++ b/internal/modules/entity/service.go @@ -4,6 +4,7 @@ package entity import ( "udap/internal/core/domain" + "udap/internal/log" ) type entityService struct { @@ -23,9 +24,13 @@ func (u entityService) Config(id string, value string) error { return nil } -func (u entityService) Register(entity *domain.Entity) (*domain.Entity, error) { - // TODO implement me - panic("implement me") +func (u entityService) Register(entity *domain.Entity) error { + err := u.repository.Register(entity) + if err != nil { + return err + } + log.Event("Entity '%s' registered.", entity.Name) + return nil } func NewService(repository domain.EntityRepository) domain.EntityService { diff --git a/internal/modules/module/module.go b/internal/modules/module/module.go index 1e6a37b..d19213d 100644 --- a/internal/modules/module/module.go +++ b/internal/modules/module/module.go @@ -4,11 +4,13 @@ package module import ( "gorm.io/gorm" + "udap/internal/controller" "udap/internal/core/domain" ) -func New(db *gorm.DB) domain.ModuleService { +func New(db *gorm.DB, controller *controller.Controller) domain.ModuleService { repo := NewRepository(db) - service := NewService(repo) + operator := NewOperator(controller) + service := NewService(repo, operator) return service } diff --git a/internal/modules/module/operator.go b/internal/modules/module/operator.go new file mode 100644 index 0000000..0f12fa8 --- /dev/null +++ b/internal/modules/module/operator.go @@ -0,0 +1,100 @@ +// Copyright (c) 2022 Braden Nicholson + +package module + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "strings" + "time" + "udap/internal/controller" + "udap/internal/core/domain" + "udap/internal/log" + "udap/pkg/plugin" +) + +type moduleOperator struct { + ctrl *controller.Controller + modules map[string]plugin.ModuleInterface +} + +func (m moduleOperator) Update(module *domain.Module) error { + err := m.modules[module.Name].Update() + if err != nil { + return err + } + return nil +} + +func (m moduleOperator) Run(module *domain.Module) error { + err := m.modules[module.Name].Run() + if err != nil { + return err + } + return nil +} + +func NewOperator(ctrl *controller.Controller) domain.ModuleOperator { + return &moduleOperator{ + ctrl: ctrl, + modules: map[string]plugin.ModuleInterface{}, + } +} + +func (m moduleOperator) Build(module *domain.Module) error { + start := time.Now() + if _, err := os.Stat(module.Path); err != nil { + return err + } + // Create a timeout to prevent modules from taking too long to build + timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*15) + // Cancel the timeout of it exits before the timeout is up + defer cancelFunc() + binary := strings.Replace(module.Path, ".go", ".so", 1) + // Prepare the command arguments + args := []string{"build", "-v", "-buildmode=plugin", "-o", binary, module.Path} + // Initialize the command structure + cmd := exec.CommandContext(timeout, "go", args...) + // Run and get the stdout and stderr from the output + output, err := cmd.CombinedOutput() + if err != nil { + log.ErrF(errors.New(string(output)), "Module '%s' build failed:", module.Name) + return nil + } + log.Event("Module '%s' compiled successfully (%s)", module.Name, time.Since(start).Truncate(time.Millisecond).String()) + return nil +} + +func (m moduleOperator) Load(module *domain.Module) error { + binary := strings.Replace(module.Path, ".go", ".so", 1) + p, err := plugin.Load(binary) + if err != nil { + return err + } + mod := p.(plugin.ModuleInterface) + if mod == nil { + return fmt.Errorf("cannot read module") + } + err = mod.Connect(m.ctrl) + if err != nil { + return err + } + setup, err := mod.Setup() + if err != nil { + return err + } + + module.Name = setup.Name + module.Type = setup.Type + module.Version = setup.Version + module.Author = setup.Author + module.Description = setup.Description + + m.modules[module.Name] = mod + + log.Event("Module '%s' loaded.", module.Name) + return nil +} diff --git a/internal/modules/module/service.go b/internal/modules/module/service.go index 898ac55..55236e7 100644 --- a/internal/modules/module/service.go +++ b/internal/modules/module/service.go @@ -3,15 +3,10 @@ package module import ( - "context" - "errors" "fmt" - "os" - "os/exec" "path/filepath" "strings" "sync" - "time" "udap/internal/core/domain" "udap/internal/log" ) @@ -20,10 +15,96 @@ const DIR = "modules" type moduleService struct { repository domain.ModuleRepository + operator domain.ModuleOperator } -func NewService(repository domain.ModuleRepository) domain.ModuleService { - return moduleService{repository: repository} +func (u *moduleService) Update(module *domain.Module) error { + return u.operator.Update(module) +} + +func (u *moduleService) Run(module *domain.Module) error { + return u.operator.Run(module) +} + +func (u *moduleService) Load(module *domain.Module) error { + err := u.operator.Load(module) + if err != nil { + return err + } + err = u.repository.Update(module) + if err != nil { + return err + } + return nil +} + +func (u *moduleService) Build(module *domain.Module) error { + return u.operator.Build(module) +} + +func (u *moduleService) UpdateAll() error { + modules, err := u.repository.FindAll() + if err != nil { + return err + } + wg := sync.WaitGroup{} + wg.Add(len(*modules)) + for _, module := range *modules { + go func(mod domain.Module) { + defer wg.Done() + err = u.Update(&mod) + if err != nil { + log.Err(err) + } + }(module) + } + wg.Wait() + return nil +} + +func (u *moduleService) RunAll() error { + modules, err := u.repository.FindAll() + if err != nil { + return err + } + + for _, module := range *modules { + go func(mod domain.Module) { + err = u.Run(&mod) + if err != nil { + log.Err(err) + } + }(module) + } + + return nil +} + +func (u *moduleService) LoadAll() error { + modules, err := u.repository.FindAll() + if err != nil { + return err + } + wg := sync.WaitGroup{} + wg.Add(len(*modules)) + for _, module := range *modules { + go func(mod domain.Module) { + defer wg.Done() + err = u.Load(&mod) + if err != nil { + log.Err(err) + } + }(module) + } + wg.Wait() + return nil +} + +func NewService(repository domain.ModuleRepository, operator domain.ModuleOperator) domain.ModuleService { + return &moduleService{ + repository: repository, + operator: operator, + } } func (u moduleService) Discover() error { @@ -51,30 +132,6 @@ func (u moduleService) Discover() error { return nil } -func (u moduleService) Build(module *domain.Module) error { - start := time.Now() - if _, err := os.Stat(module.Path); err != nil { - return err - } - // Create a timeout to prevent modules from taking too long to build - timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*15) - // Cancel the timeout of it exits before the timeout is up - defer cancelFunc() - binary := strings.Replace(module.Path, ".go", ".so", 1) - // Prepare the command arguments - args := []string{"build", "-v", "-buildmode=plugin", "-o", binary, module.Path} - // Initialize the command structure - cmd := exec.CommandContext(timeout, "go", args...) - // Run and get the stdout and stderr from the output - output, err := cmd.CombinedOutput() - if err != nil { - log.ErrF(errors.New(string(output)), "Module '%s' build failed:", module.Name) - return nil - } - log.Event("Module '%s' compiled successfully (%s)", module.Name, time.Since(start).Truncate(time.Millisecond).String()) - return nil -} - func (u moduleService) BuildAll() error { modules, err := u.repository.FindAll() if err != nil { diff --git a/internal/modules/network/network.go b/internal/modules/network/network.go new file mode 100644 index 0000000..3f6a2f6 --- /dev/null +++ b/internal/modules/network/network.go @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Braden Nicholson + +package network + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func New(db *gorm.DB) domain.NetworkService { + repo := NewRepository(db) + service := NewService(repo) + return service +} diff --git a/internal/modules/network/repository.go b/internal/modules/network/repository.go new file mode 100644 index 0000000..cf4b617 --- /dev/null +++ b/internal/modules/network/repository.go @@ -0,0 +1,89 @@ +// Copyright (c) 2022 Braden Nicholson + +package network + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type networkRepo struct { + db *gorm.DB +} + +func (u networkRepo) Register(network *domain.Network) error { + if network.Id == "" { + err := u.db.Model(&domain.Network{}).Where("name = ?", network.Name).FirstOrCreate(network).Error + if err != nil { + return err + } + } else { + err := u.db.Model(&domain.Network{}).Where("id = ?", network.Id).First(network).Error + if err != nil { + return err + } + } + err := u.db.Model(&domain.Network{}).Where("id = ?", network.Id).Save(network).Error + if err != nil { + return err + } + return nil +} + +func (u networkRepo) FindByName(name string) (*domain.Network, error) { + var target *domain.Network + if err := u.db.Where("name = ?", name).Find(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func NewRepository(db *gorm.DB) domain.NetworkRepository { + return &networkRepo{ + db: db, + } +} + +func (u networkRepo) FindAll() ([]*domain.Network, error) { + var target []*domain.Network + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u networkRepo) FindById(id string) (*domain.Network, error) { + var target *domain.Network + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u networkRepo) Create(network *domain.Network) error { + if err := u.db.Create(network).Error; err != nil { + return err + } + return nil +} + +func (u networkRepo) FindOrCreate(network *domain.Network) error { + if err := u.db.FirstOrCreate(network).Error; err != nil { + return err + } + return nil +} + +func (u networkRepo) Update(network *domain.Network) error { + if err := u.db.Save(network).Error; err != nil { + return err + } + return nil +} + +func (u networkRepo) Delete(network *domain.Network) error { + if err := u.db.Delete(network).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/network/service.go b/internal/modules/network/service.go new file mode 100644 index 0000000..f589a24 --- /dev/null +++ b/internal/modules/network/service.go @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Braden Nicholson + +package network + +import ( + "udap/internal/core/domain" +) + +type networkService struct { + repository domain.NetworkRepository +} + +func (u networkService) Register(network *domain.Network) error { + return u.repository.Register(network) +} + +func NewService(repository domain.NetworkRepository) domain.NetworkService { + return networkService{repository: repository} +} + +// Repository Mapping + +func (u networkService) FindAll() ([]*domain.Network, error) { + return u.repository.FindAll() +} + +func (u networkService) FindById(id string) (*domain.Network, error) { + return u.repository.FindById(id) +} + +func (u networkService) Create(network *domain.Network) error { + return u.repository.Create(network) +} + +func (u networkService) FindOrCreate(network *domain.Network) error { + return u.repository.FindOrCreate(network) +} + +func (u networkService) Update(network *domain.Network) error { + return u.repository.Update(network) +} + +func (u networkService) Delete(network *domain.Network) error { + return u.repository.Delete(network) +} diff --git a/internal/modules/zone/repository.go b/internal/modules/zone/repository.go new file mode 100644 index 0000000..a445f90 --- /dev/null +++ b/internal/modules/zone/repository.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Braden Nicholson + +package zone + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +type zoneRepo struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) domain.ZoneRepository { + return &zoneRepo{ + db: db, + } +} + +func (u zoneRepo) FindAll() ([]*domain.Zone, error) { + var target []*domain.Zone + if err := u.db.First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u zoneRepo) FindById(id string) (*domain.Zone, error) { + var target *domain.Zone + if err := u.db.Where("id = ?", id).First(target).Error; err != nil { + return nil, err + } + return target, nil +} + +func (u zoneRepo) Create(zone *domain.Zone) error { + if err := u.db.Create(zone).Error; err != nil { + return err + } + return nil +} + +func (u zoneRepo) FindOrCreate(zone *domain.Zone) error { + if err := u.db.FirstOrCreate(zone).Error; err != nil { + return err + } + return nil +} + +func (u zoneRepo) Update(zone *domain.Zone) error { + if err := u.db.Save(zone).Error; err != nil { + return err + } + return nil +} + +func (u zoneRepo) Delete(zone *domain.Zone) error { + if err := u.db.Delete(zone).Error; err != nil { + return err + } + return nil +} diff --git a/internal/modules/zone/service.go b/internal/modules/zone/service.go new file mode 100644 index 0000000..972844c --- /dev/null +++ b/internal/modules/zone/service.go @@ -0,0 +1,41 @@ +// Copyright (c) 2022 Braden Nicholson + +package zone + +import ( + "udap/internal/core/domain" +) + +type zoneService struct { + repository domain.ZoneRepository +} + +func NewService(repository domain.ZoneRepository) domain.ZoneService { + return zoneService{repository: repository} +} + +// Repository Mapping + +func (u zoneService) FindAll() ([]*domain.Zone, error) { + return u.repository.FindAll() +} + +func (u zoneService) FindById(id string) (*domain.Zone, error) { + return u.repository.FindById(id) +} + +func (u zoneService) Create(zone *domain.Zone) error { + return u.repository.Create(zone) +} + +func (u zoneService) FindOrCreate(zone *domain.Zone) error { + return u.repository.FindOrCreate(zone) +} + +func (u zoneService) Update(zone *domain.Zone) error { + return u.repository.Update(zone) +} + +func (u zoneService) Delete(zone *domain.Zone) error { + return u.repository.Delete(zone) +} diff --git a/internal/modules/zone/zone.go b/internal/modules/zone/zone.go new file mode 100644 index 0000000..8471031 --- /dev/null +++ b/internal/modules/zone/zone.go @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Braden Nicholson + +package zone + +import ( + "gorm.io/gorm" + "udap/internal/core/domain" +) + +func New(db *gorm.DB) domain.ZoneService { + repo := NewRepository(db) + service := NewService(repo) + return service +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index fd985ba..a9df96c 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -6,34 +6,115 @@ import ( "github.com/go-chi/chi" "gorm.io/gorm" "net/http" + "time" "udap/internal/controller" + "udap/internal/core" + "udap/internal/core/domain" "udap/internal/log" + "udap/internal/modules/module" "udap/internal/port/routes" "udap/internal/port/runtimes" + "udap/internal/pulse" "udap/platform/database" "udap/platform/router" ) type orchestrator struct { - db *gorm.DB - router chi.Router + db *gorm.DB + router chi.Router + server *http.Server + controller *controller.Controller + + modules domain.ModuleService +} + +func (o *orchestrator) Update() error { + endpoints, err := o.controller.Endpoints.FindAll() + if err != nil { + return err + } + eps := *endpoints + for i := range eps { + ep := eps[i] + err = o.controller.Endpoints.Send(ep.Id, "endpoint", ep) + if err != nil { + return nil + } + } + err = o.controller.Attributes.EmitAll() + if err != nil { + return err + } + return nil +} + +func (o *orchestrator) Timings() error { + timings := pulse.Timings.Timings() + for _, timing := range timings { + err := o.controller.Endpoints.Send("", "timing", timing) + if err != nil { + return err + } + } + return nil } -func (o orchestrator) Run() error { +func (o *orchestrator) Run() error { - server := &http.Server{Addr: ":8080", Handler: o.router} + o.server = &http.Server{Addr: ":3020", Handler: o.router} - err := server.ListenAndServe() + go func() { + err := o.server.ListenAndServe() + if err != nil { + log.ErrF(err, "http server exited with error:\n") + } + }() + + attrs := make(chan domain.Attribute) + err := o.controller.Attributes.Watch(attrs) if err != nil { - log.ErrF(err, "http server exited with error:\n") + return err + } + go func() { + for attr := range attrs { + err = o.controller.Endpoints.Send("", "attribute", attr) + if err != nil { + return + } + } + }() + + delay := 1000.0 + for { + select { + case <-time.After(time.Millisecond * time.Duration(delay)): + log.Event("Update timed out") + default: + start := time.Now() + err := o.Update() + if err != nil { + log.ErrF(err, "runtime update error: %s") + } + d := time.Since(start) + dur := (time.Millisecond * time.Duration(delay)) - d + if dur > 0 { + time.Sleep(dur) + } + } + + err := o.Timings() + if err != nil { + return err + } + } return nil } type Orchestrator interface { - Init() error + Start() error Run() error } @@ -53,19 +134,27 @@ func NewOrchestrator() Orchestrator { } } -func (o orchestrator) Init() error { +func (o *orchestrator) Start() error { var err error + + err = core.MigrateModels(o.db) + if err != nil { + return err + } + o.controller, err = controller.NewController(o.db) if err != nil { return err } + o.modules = module.New(o.db, o.controller) + // Initialize and route applicable domains routes.NewUserRouter(o.controller.Users).RouteUsers(o.router) routes.NewEndpointRouter(o.controller.Endpoints).RouteEndpoints(o.router) - routes.NewModuleRouter(o.controller.Modules).RouteModules(o.router) + routes.NewModuleRouter(o.modules).RouteModules(o.router) - runtimes.NewModuleRuntime(o.controller.Modules) + runtimes.NewModuleRuntime(o.modules) return nil } diff --git a/internal/port/routes/endpoint.go b/internal/port/routes/endpoint.go index 1d601a3..571808c 100644 --- a/internal/port/routes/endpoint.go +++ b/internal/port/routes/endpoint.go @@ -5,8 +5,10 @@ package routes import ( "encoding/json" "github.com/go-chi/chi" + "github.com/gorilla/websocket" "net/http" "udap/internal/core/domain" + "udap/internal/log" "udap/platform/jwt" ) @@ -25,8 +27,9 @@ func NewEndpointRouter(service domain.EndpointService) EndpointRouter { } func (r endpointRouter) RouteEndpoints(router chi.Router) { + router.Get("/socket/{token}", r.enroll) router.Route("/endpoints", func(local chi.Router) { - local.Post("/authenticate/{key}", r.authenticate) + local.Get("/register/{key}", r.authenticate) }) } @@ -38,11 +41,13 @@ func (r endpointRouter) authenticate(w http.ResponseWriter, req *http.Request) { key := chi.URLParam(req, "key") if key == "" { http.Error(w, "access key not provided", 401) + return } endpoint, err := r.service.FindByKey(key) if err != nil { http.Error(w, "invalid endpoint name", 401) + return } token, err := jwt.SignUUID(endpoint.Id) @@ -66,3 +71,36 @@ func (r endpointRouter) authenticate(w http.ResponseWriter, req *http.Request) { } w.WriteHeader(200) } + +func (r endpointRouter) enroll(w http.ResponseWriter, req *http.Request) { + // Initialize an error to manage returns + var err error + // Convert the basic GET request into a WebSocket session + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + // Upgrade the https session to a web socket session + conn, err := upgrader.Upgrade(w, req, nil) + if err != nil { + log.Err(err) + return + } + // find the auth token in the url params + tokenParam := chi.URLParam(req, "token") + // Defer the termination of the session to function return + id, err := jwt.AuthToken(tokenParam) + if err != nil { + log.Err(err) + return + } + + err = r.service.Enroll(id, conn) + if err != nil { + return + } + +} diff --git a/internal/port/runtimes/module.go b/internal/port/runtimes/module.go index 532961b..84921ad 100644 --- a/internal/port/runtimes/module.go +++ b/internal/port/runtimes/module.go @@ -19,4 +19,14 @@ func NewModuleRuntime(service domain.ModuleService) { return } + err = service.LoadAll() + if err != nil { + return + } + + err = service.RunAll() + if err != nil { + return + } + } diff --git a/internal/store/database.go b/internal/store/database.go deleted file mode 100644 index 67afef7..0000000 --- a/internal/store/database.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package store - -import ( - "fmt" - "gorm.io/driver/postgres" - _ "gorm.io/driver/postgres" - "gorm.io/gorm" - "os" - "time" -) - -var DB Database - -type Persistent struct { - CreatedAt time.Time `json:"created"` - UpdatedAt time.Time `json:"updated"` - Deleted bool `json:"deleted"` - deletedAt *time.Time `sql:"index"` - // Id is primary key of the persistent type, represented as a UUIDv4 - Id string `json:"id" gorm:"primary_key;type:string;default:uuid_generate_v4()"` -} - -type Database struct { - *gorm.DB -} - -func NewDatabase() (Database, error) { - pg := postgres.Open(dbURL()) - db, err := gorm.Open(pg, &gorm.Config{}) - if err != nil { - return Database{}, err - } - - DB.DB = db - - return DB, nil -} - -// dbURL returns a formatted postgresql connection string. -func dbURL() string { - // The credentials are retrieved from the OS environment - dbUser := os.Getenv("dbUser") - dbPass := os.Getenv("dbPass") - // Host and port are also obtained from the environment - dbHost := os.Getenv("dbHost") - dbPort := os.Getenv("dbPort") - // The name of the database is again retrieved from the environment - dbName := os.Getenv("dbName") - // All variables are aggregated into the connection url - u := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", dbHost, dbUser, dbPass, dbName, dbPort) - return u -} diff --git a/internal/udap/runner.go b/internal/udap/runner.go deleted file mode 100644 index d3e178d..0000000 --- a/internal/udap/runner.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package udap - -func Begin() { - -} diff --git a/internal/udap/udap.go b/internal/udap/udap.go deleted file mode 100644 index 95c2107..0000000 --- a/internal/udap/udap.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2021 Braden Nicholson - -package udap - -import ( - "fmt" - "github.com/joho/godotenv" - "os" - "udap/internal/log" - "udap/internal/models" - "udap/internal/server" - "udap/internal/store" -) - -const VERSION = "2.12" - -type Udap struct { - runtime *server.Runtime -} - -func (u Udap) startup() error { - log.Log("UDAP v%s - Copyright (c) 2019-2022 Braden Nicholson", VERSION) - - err := godotenv.Load() - if err != nil { - return fmt.Errorf("failed to load .env file") - } - - if os.Getenv("environment") == "production" { - log.Log("Running in PRODUCTION mode.") - } else { - log.Log("Running in DEVELOPMENT mode.") - } - - err = os.Setenv("version", VERSION) - if err != nil { - return err - } - - return nil -} - -func Start() error { - u := &Udap{} - err := u.startup() - if err != nil { - return err - } - - _, err = store.NewDatabase() - if err != nil { - return err - } - - err = models.MigrateModels() - if err != nil { - return err - } - - u.runtime = &server.Runtime{} - - err = u.runtime.Load() - if err != nil { - return err - } - - err = u.runtime.Run() - if err != nil { - return err - } - return nil -} diff --git a/modules/govee/govee.go b/modules/govee/govee.go index 0aa54e3..5d68b15 100644 --- a/modules/govee/govee.go +++ b/modules/govee/govee.go @@ -13,8 +13,8 @@ import ( "strconv" "sync" "time" + "udap/internal/core/domain" "udap/internal/log" - "udap/internal/models" "udap/pkg/plugin" ) @@ -421,52 +421,56 @@ func (g *Govee) setState(device Device, value string, mode string, id string) er return nil } -func (g *Govee) statePut(device Device, mode string, id string) models.FuncPut { +func (g *Govee) statePut(device Device, mode string, id string) func(value string) error { return func(value string) error { return g.setState(device, value, mode, id) } } -func (g *Govee) stateGet(device Device, mode string, id string) models.FuncGet { +func (g *Govee) stateGet(device Device, mode string, id string) func() (string, error) { return func() (string, error) { return g.getSingleState(device, mode) } } -func GenerateAttributes(id string) []*models.Attribute { - on := models.Attribute{ +func GenerateAttributes(id string) []*domain.Attribute { + on := domain.Attribute{ Key: "on", Value: "false", Request: "false", Type: "toggle", Order: 0, Entity: id, + Channel: make(chan domain.Attribute), } - dim := models.Attribute{ + dim := domain.Attribute{ Key: "dim", Value: "0", Request: "0", Type: "range", Order: 1, Entity: id, + Channel: make(chan domain.Attribute), } - cct := models.Attribute{ + cct := domain.Attribute{ Key: "cct", Value: "2000", Request: "2000", Type: "range", Order: 3, Entity: id, + Channel: make(chan domain.Attribute), } - hue := models.Attribute{ + hue := domain.Attribute{ Key: "hue", Value: "0", Request: "0", Type: "range", Order: 4, Entity: id, + Channel: make(chan domain.Attribute), } - return []*models.Attribute{&on, &dim, &hue, &cct} + return []*domain.Attribute{&on, &dim, &hue, &cct} } func (g *Govee) Setup() (plugin.Config, error) { @@ -507,16 +511,26 @@ func (g *Govee) Run() error { for _, device := range devices { - s := models.NewSpectrum(device.DeviceName, g.Config.Name) - _, err = g.Entities.Register(s) + s := &domain.Entity{ + Name: device.DeviceName, + Type: "spectrum", + Module: g.Config.Name, + } + err = g.Entities.Register(s) if err != nil { return err } g.devices[s.Id] = device attributes := GenerateAttributes(s.Id) for _, attribute := range attributes { - attribute.FnGet(g.stateGet(device, attribute.Key, s.Id)) - attribute.FnPut(g.statePut(device, attribute.Key, s.Id)) + go func() { + for attr := range attribute.Channel { + err = g.statePut(device, attribute.Key, s.Id)(attr.Request) + if err != nil { + return + } + } + }() err = g.Attributes.Register(attribute) if err != nil { return err diff --git a/modules/hs100/hs100.go b/modules/hs100/hs100.go index 782e806..f5b2fdc 100644 --- a/modules/hs100/hs100.go +++ b/modules/hs100/hs100.go @@ -9,7 +9,6 @@ import ( "strings" "time" "udap/internal/core/domain" - "udap/internal/models" "udap/pkg/plugin" ) @@ -53,7 +52,7 @@ func (h *HS100) findDevices() error { Type: "switch", Module: "hs100", } - _, err = h.Entities.Register(&newSwitch) + err = h.Entities.Register(&newSwitch) if err != nil { return err } @@ -132,7 +131,7 @@ func (h *HS100) Run() (err error) { return nil } -func (h *HS100) put(device *hs100.Hs100) models.FuncPut { +func (h *HS100) put(device *hs100.Hs100) func(s string) error { return func(s string) error { parseBool, err := strconv.ParseBool(s) @@ -154,7 +153,7 @@ func (h *HS100) put(device *hs100.Hs100) models.FuncPut { } } -func (h *HS100) get(device *hs100.Hs100) models.FuncGet { +func (h *HS100) get(device *hs100.Hs100) func() (string, error) { return func() (string, error) { on, err := device.IsOn() if err != nil { diff --git a/modules/macmeta/macmeta.go b/modules/macmeta/macmeta.go index b38edfd..650f524 100644 --- a/modules/macmeta/macmeta.go +++ b/modules/macmeta/macmeta.go @@ -33,7 +33,7 @@ func (v *MacMeta) createDisplaySwitch() error { Type: "switch", Module: "macmeta", } - _, err := v.Entities.Register(newSwitch) + err := v.Entities.Register(newSwitch) if err != nil { return err } diff --git a/modules/spotify/spotify.go b/modules/spotify/spotify.go index beabe79..b705dcf 100644 --- a/modules/spotify/spotify.go +++ b/modules/spotify/spotify.go @@ -16,7 +16,6 @@ import ( "time" "udap/internal/core/domain" "udap/internal/log" - "udap/internal/models" "udap/pkg/plugin" ) @@ -40,7 +39,7 @@ func init() { Module.Config = config } -func (s *Spotify) PutAttribute(key string) models.FuncPut { +func (s *Spotify) PutAttribute(key string) func(str string) error { return func(str string) error { switch key { case "current": @@ -82,7 +81,7 @@ func (s *Spotify) PutAttribute(key string) models.FuncPut { } } -func (s *Spotify) GetAttribute(key string) models.FuncGet { +func (s *Spotify) GetAttribute(key string) func() (string, error) { return func() (string, error) { switch key { case "current": @@ -314,7 +313,7 @@ func (s *Spotify) Run() error { Type: "media", Module: "spotify", } - _, err := s.Entities.Register(e) + err := s.Entities.Register(e) if err != nil { return err } diff --git a/modules/squid/squid.go b/modules/squid/squid.go index 1241fdc..5d2d718 100644 --- a/modules/squid/squid.go +++ b/modules/squid/squid.go @@ -9,8 +9,8 @@ import ( "strconv" "sync" "time" + "udap/internal/core/domain" "udap/internal/log" - "udap/internal/models" "udap/pkg/dmx" "udap/pkg/dmx/ft232" "udap/pkg/plugin" @@ -170,43 +170,60 @@ func (s *Squid) registerDevices() error { } for i := 1; i <= 16; i++ { - entity := models.NewDimmer(fmt.Sprintf("ch%d", i), s.Config.Name) - - res, err := s.Entities.Register(entity) + entity := &domain.Entity{ + Name: fmt.Sprintf("ch%d", i), + Module: s.Config.Name, + Type: "dimmer", + } + err := s.Entities.Register(entity) if err != nil { return err } - on := models.Attribute{ + on := domain.Attribute{ Key: "on", Value: "false", Request: "false", Type: "toggle", Order: 0, - Entity: res.Id, + Entity: entity.Id, + Channel: make(chan domain.Attribute), } - s.entities[i] = res.Id + s.entities[i] = entity.Id - on.FnGet(s.remoteGetOn(i)) - on.FnPut(s.remotePutOn(i)) + go func() { + for attribute := range on.Channel { + err = s.remotePutOn(i)(attribute.Request) + if err != nil { + return + } + } + }() err = s.Attributes.Register(&on) if err != nil { return err } - dim := models.Attribute{ + dim := domain.Attribute{ Key: "dim", Value: "0", Request: "0", Type: "range", Order: 1, - Entity: res.Id, + Entity: entity.Id, + Channel: make(chan domain.Attribute), } - dim.FnGet(s.remoteGetDim(i)) - dim.FnPut(s.remotePutDim(i)) + go func() { + for attribute := range dim.Channel { + err = s.remotePutDim(i)(attribute.Request) + if err != nil { + return + } + } + }() err = s.Attributes.Register(&dim) if err != nil { @@ -231,7 +248,6 @@ func (s *Squid) connect() error { s.connected = false so := os.Stdout - os.Stdout = os.DevNull config := dmx.NewConfig(0x02) config.GetUSBContext() diff --git a/modules/vyos/vyos.go b/modules/vyos/vyos.go index b9c6800..2bb1766 100644 --- a/modules/vyos/vyos.go +++ b/modules/vyos/vyos.go @@ -14,8 +14,8 @@ import ( "strings" "sync" "time" + "udap/internal/core/domain" "udap/internal/log" - "udap/internal/models" "udap/pkg/plugin" ) @@ -54,7 +54,7 @@ func (v *Vyos) Run() error { return nil } -func (v *Vyos) scanSubnet(network models.Network) error { +func (v *Vyos) scanSubnet(network domain.Network) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() @@ -80,7 +80,8 @@ func (v *Vyos) scanSubnet(network models.Network) error { } // Use the results to print an example output for _, host := range result.Hosts { - device := models.NewDevice() + + device := domain.Device{} for _, addr := range host.Addresses { switch addr.AddrType { @@ -94,7 +95,7 @@ func (v *Vyos) scanSubnet(network models.Network) error { } device.NetworkId = network.Id - _, err = v.Devices.Register(device) + err = v.Devices.Register(&device) if err != nil { return err } @@ -162,7 +163,7 @@ func (v *Vyos) fetchNetworks() error { wg := sync.WaitGroup{} for name, lan := range d.Networks { - network := models.Network{} + network := domain.Network{} network.Name = name network.Dns = strings.Join(lan.NameServer, ",") for s, subnet := range lan.Subnets { @@ -173,7 +174,7 @@ func (v *Vyos) fetchNetworks() error { break } - _, err = v.Networks.Register(&network) + err = v.Networks.Register(&network) if err != nil { log.Err(err) } diff --git a/modules/weather/weather.go b/modules/weather/weather.go index 82c80fb..f361882 100644 --- a/modules/weather/weather.go +++ b/modules/weather/weather.go @@ -181,7 +181,7 @@ func (v *Weather) Run() error { Module: "weather", Type: "media", } - _, err = v.Entities.Register(e) + err = v.Entities.Register(e) if err != nil { return err } diff --git a/internal/store/database_test.go b/platform/database/database_test.go similarity index 89% rename from internal/store/database_test.go rename to platform/database/database_test.go index b7b7a96..1fea358 100644 --- a/internal/store/database_test.go +++ b/platform/database/database_test.go @@ -1,6 +1,6 @@ -// Copyright (c) 2021 Braden Nicholson +// Copyright (c) 2022 Braden Nicholson -package store +package database import ( "fmt" From b942ed9a8a7009282e49e2110a5eb02e6ecf89ae Mon Sep 17 00:00:00 2001 From: Braden Date: Tue, 31 May 2022 16:32:20 -0700 Subject: [PATCH 05/18] Prolific improvements to various systems. Aggregated all domain services to the controller to be shared with endpoints and modules. Added module implementation for Atlas. Reorganized the modules. Added generic interfaces for basic crud operations in the repository. Added vosk voice recognition for atlas. Added mimic speech synthesis for atlas. Introduced the framework for endpoints, basic implementation is now in working order. Generified more of the interfaces. --- internal/controller/controller.go | 106 +++++----- internal/core/domain/attribute.go | 15 +- .../core/domain/{ => common}/persistent.go | 11 +- internal/core/domain/device.go | 14 +- internal/core/domain/endpoint.go | 18 +- internal/core/domain/entity.go | 12 +- internal/core/domain/module.go | 12 +- internal/core/domain/mutation.go | 22 ++ internal/core/domain/network.go | 14 +- internal/core/domain/user.go | 14 +- internal/core/domain/zone.go | 14 +- .../{ => core}/modules/attribute/attribute.go | 0 .../{ => core}/modules/attribute/operator.go | 13 +- .../modules/attribute/repository.go | 18 +- .../{ => core}/modules/attribute/service.go | 49 +++-- internal/{ => core}/modules/device/device.go | 0 .../{ => core}/modules/device/repository.go | 8 +- internal/{ => core}/modules/device/service.go | 42 +++- .../{ => core}/modules/endpoint/endpoint.go | 5 +- internal/core/modules/endpoint/operator.go | 194 ++++++++++++++++++ .../{ => core}/modules/endpoint/repository.go | 0 .../{ => core}/modules/endpoint/service.go | 58 +++++- .../{ => core}/modules/endpoint/system.go | 0 internal/{ => core}/modules/entity/entity.go | 0 .../{ => core}/modules/entity/repository.go | 6 +- internal/{ => core}/modules/entity/service.go | 48 ++++- internal/{ => core}/modules/module/module.go | 0 .../{ => core}/modules/module/operator.go | 30 +-- .../{ => core}/modules/module/repository.go | 0 internal/{ => core}/modules/module/service.go | 46 ++++- .../{ => core}/modules/network/network.go | 0 .../{ => core}/modules/network/repository.go | 14 +- .../{ => core}/modules/network/service.go | 42 +++- .../{ => core}/modules/user/repository.go | 8 +- internal/{ => core}/modules/user/service.go | 41 +++- internal/{ => core}/modules/user/user.go | 0 .../{ => core}/modules/zone/repository.go | 8 +- internal/{ => core}/modules/zone/service.go | 42 +++- internal/{ => core}/modules/zone/zone.go | 0 internal/log/log.go | 4 +- internal/modules/endpoint/operator.go | 69 ------- internal/orchestrator/orchestrator.go | 170 ++++++++------- internal/port/routes/endpoint.go | 12 +- modules/atlas/atlas.go | 177 ++++++++++++++++ modules/homekit/homekit.go | 2 +- modules/squid/squid.go | 6 +- modules/vyos/vyos.go | 2 +- modules/weather/weather.go | 11 +- modules/webstats/webstats.go | 4 - pkg/plugin/default.go | 2 +- 50 files changed, 1003 insertions(+), 380 deletions(-) rename internal/core/domain/{ => common}/persistent.go (63%) create mode 100644 internal/core/domain/mutation.go rename internal/{ => core}/modules/attribute/attribute.go (100%) rename internal/{ => core}/modules/attribute/operator.go (73%) rename internal/{ => core}/modules/attribute/repository.go (71%) rename internal/{ => core}/modules/attribute/service.go (65%) rename internal/{ => core}/modules/device/device.go (100%) rename internal/{ => core}/modules/device/repository.go (86%) rename internal/{ => core}/modules/device/service.go (53%) rename internal/{ => core}/modules/endpoint/endpoint.go (57%) create mode 100644 internal/core/modules/endpoint/operator.go rename internal/{ => core}/modules/endpoint/repository.go (100%) rename internal/{ => core}/modules/endpoint/service.go (55%) rename internal/{ => core}/modules/endpoint/system.go (100%) rename internal/{ => core}/modules/entity/entity.go (100%) rename internal/{ => core}/modules/entity/repository.go (95%) rename internal/{ => core}/modules/entity/service.go (61%) rename internal/{ => core}/modules/module/module.go (100%) rename internal/{ => core}/modules/module/operator.go (80%) rename internal/{ => core}/modules/module/repository.go (100%) rename internal/{ => core}/modules/module/service.go (81%) rename internal/{ => core}/modules/network/network.go (100%) rename internal/{ => core}/modules/network/repository.go (85%) rename internal/{ => core}/modules/network/service.go (54%) rename internal/{ => core}/modules/user/repository.go (86%) rename internal/{ => core}/modules/user/service.go (68%) rename internal/{ => core}/modules/user/user.go (100%) rename internal/{ => core}/modules/zone/repository.go (86%) rename internal/{ => core}/modules/zone/service.go (50%) rename internal/{ => core}/modules/zone/zone.go (100%) delete mode 100644 internal/modules/endpoint/operator.go create mode 100644 modules/atlas/atlas.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index a1d1d20..907a277 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -3,38 +3,34 @@ package controller import ( - "fmt" "gorm.io/gorm" "udap/internal/bond" "udap/internal/core/domain" - "udap/internal/modules/attribute" - "udap/internal/modules/device" - "udap/internal/modules/endpoint" - "udap/internal/modules/entity" - "udap/internal/modules/network" - "udap/internal/modules/user" - "udap/internal/modules/zone" - "udap/internal/pulse" + "udap/internal/core/modules/attribute" + "udap/internal/core/modules/device" + "udap/internal/core/modules/entity" + "udap/internal/core/modules/network" + "udap/internal/core/modules/user" + "udap/internal/core/modules/zone" ) type Controller struct { Attributes domain.AttributeService Devices domain.DeviceService - Endpoints domain.EndpointService Entities domain.EntityService Networks domain.NetworkService Users domain.UserService Zones domain.ZoneService + Endpoints domain.EndpointService + Modules domain.ModuleService event chan bond.Msg } func NewController(db *gorm.DB) (*Controller, error) { c := &Controller{} - c.Attributes = attribute.New(db) - c.Devices = device.New(db) - c.Endpoints = endpoint.New(db) c.Entities = entity.New(db) + c.Devices = device.New(db) c.Networks = network.New(db) c.Users = user.New(db) c.Zones = zone.New(db) @@ -42,53 +38,51 @@ func NewController(db *gorm.DB) (*Controller, error) { return c, nil } -func (c *Controller) Handle(msg bond.Msg) (interface{}, error) { - - pulse.LogGlobal("-> Ctrl::%s %s", msg.Target, msg.Operation) - - // switch t := msg.Target; t { - // case "attribute": - // return c.Attributes.Handle(msg) - // case "device": - // return c.Devices.Handle(msg) - // case "network": - // return c.Networks.Handle(msg) - // case "zone": - // return c.Zones.Handle(msg) - // default: - // return nil, fmt.Errorf("unknown target '%s'", t) - // } - return nil, nil +func (c *Controller) Listen(resp chan domain.Mutation) { + + err := c.Modules.Watch(resp) + if err != nil { + return + } + + err = c.Endpoints.Watch(resp) + if err != nil { + return + } + + err = c.Entities.Watch(resp) + if err != nil { + return + } + + err = c.Attributes.Watch(resp) + if err != nil { + return + } + } func (c *Controller) EmitAll() error { - // var err error - // err = c.Attributes.EmitAll() - // if err != nil { - // return err - // } - // - // err = c.Networks.EmitAll() - // if err != nil { - // return err - // } - // - // err = c.Devices.EmitAll() - // if err != nil { - // return err - // } - // - // err = c.Zones.EmitAll() - // if err != nil { - // return err - // } - return nil -} + err := c.Entities.EmitAll() + if err != nil { + return err + } -func (c *Controller) Meta(msg bond.Msg) error { - switch t := msg.Operation; t { - default: - return fmt.Errorf("unknown operation '%s'", t) + err = c.Attributes.EmitAll() + if err != nil { + return err } + + err = c.Modules.EmitAll() + if err != nil { + return err + } + + err = c.Endpoints.EmitAll() + if err != nil { + return err + } + + return nil } diff --git a/internal/core/domain/attribute.go b/internal/core/domain/attribute.go index fd1d421..ff9a11b 100644 --- a/internal/core/domain/attribute.go +++ b/internal/core/domain/attribute.go @@ -5,10 +5,11 @@ package domain import ( "strconv" "time" + "udap/internal/core/domain/common" ) type Attribute struct { - Persistent + common.Persistent Value string `json:"value"` Updated time.Time `json:"updated"` Request string `json:"request"` @@ -17,7 +18,7 @@ type Attribute struct { Key string `json:"key"` Type string `json:"type"` Order int `json:"order"` - Channel chan Attribute `gorm:"-"` + Channel chan Attribute `json:"-" gorm:"-"` // put FuncPut // get FuncGet } @@ -47,15 +48,10 @@ func (a *Attribute) AsBool() bool { } type AttributeRepository interface { - FindAll() (*[]Attribute, error) + common.Persist[Attribute] FindAllByEntity(entity string) (*[]Attribute, error) - FindById(id string) (*Attribute, error) FindByComposite(entity string, key string) (*Attribute, error) - Create(*Attribute) error Register(*Attribute) error - FindOrCreate(*Attribute) error - Update(*Attribute) error - Delete(*Attribute) error } type AttributeOperator interface { @@ -66,9 +62,8 @@ type AttributeOperator interface { } type AttributeService interface { + Observable FindAll() (*[]Attribute, error) - EmitAll() error - Watch(chan<- Attribute) error FindAllByEntity(entity string) (*[]Attribute, error) FindById(id string) (*Attribute, error) Create(*Attribute) error diff --git a/internal/core/domain/persistent.go b/internal/core/domain/common/persistent.go similarity index 63% rename from internal/core/domain/persistent.go rename to internal/core/domain/common/persistent.go index b932ce1..1660840 100644 --- a/internal/core/domain/persistent.go +++ b/internal/core/domain/common/persistent.go @@ -1,6 +1,6 @@ // Copyright (c) 2022 Braden Nicholson -package domain +package common import "time" @@ -11,3 +11,12 @@ type Persistent struct { deletedAt *time.Time `sql:"index"` Id string `json:"id" gorm:"primary_key;type:string;default:uuid_generate_v4()"` } + +type Persist[T any] interface { + FindAll() (*[]T, error) + FindById(id string) (*T, error) + Create(*T) error + FindOrCreate(*T) error + Update(*T) error + Delete(*T) error +} diff --git a/internal/core/domain/device.go b/internal/core/domain/device.go index 83f9fde..ac0df68 100644 --- a/internal/core/domain/device.go +++ b/internal/core/domain/device.go @@ -2,8 +2,10 @@ package domain +import "udap/internal/core/domain/common" + type Device struct { - Persistent + common.Persistent NetworkId string `json:"networkId" gorm:"-"` EntityId string `json:"entityId" gorm:"-"` Name string `json:"name"` @@ -14,16 +16,12 @@ type Device struct { } type DeviceRepository interface { - FindAll() ([]*Device, error) - FindById(id string) (*Device, error) - Create(*Device) error - FindOrCreate(*Device) error - Update(*Device) error - Delete(*Device) error + common.Persist[Device] } type DeviceService interface { - FindAll() ([]*Device, error) + Observable + FindAll() (*[]Device, error) FindById(id string) (*Device, error) Create(*Device) error FindOrCreate(*Device) error diff --git a/internal/core/domain/endpoint.go b/internal/core/domain/endpoint.go index 55c8890..9e50c15 100644 --- a/internal/core/domain/endpoint.go +++ b/internal/core/domain/endpoint.go @@ -6,10 +6,11 @@ import ( "github.com/gorilla/websocket" "math/rand" "time" + "udap/internal/core/domain/common" ) type Endpoint struct { - Persistent + common.Persistent Name string `json:"name" gorm:"unique"` Type string `json:"type" gorm:"default:'terminal'"` Connected bool `json:"connected"` @@ -30,7 +31,7 @@ func randomSequence() string { func NewEndpoint(name string, variant string) *Endpoint { return &Endpoint{ - Persistent: Persistent{}, + Persistent: common.Persistent{}, Name: name, Type: variant, Connected: false, @@ -39,18 +40,15 @@ func NewEndpoint(name string, variant string) *Endpoint { } type EndpointRepository interface { - FindAll() (*[]Endpoint, error) - FindById(id string) (*Endpoint, error) + common.Persist[Endpoint] FindByKey(key string) (*Endpoint, error) - Create(*Endpoint) error - FindOrCreate(*Endpoint) error - Update(*Endpoint) error - Delete(*Endpoint) error } type EndpointOperator interface { Enroll(*Endpoint, *websocket.Conn) error + Unenroll(id string) error Send(id string, operation string, payload any) error + SendAll(id string, operation string, payload any) error } type EndpointService interface { @@ -59,7 +57,11 @@ type EndpointService interface { FindByKey(key string) (*Endpoint, error) Create(*Endpoint) error + Observable + Enroll(id string, conn *websocket.Conn) error + + SendAll(target string, operation string, payload any) error Send(id string, operation string, payload any) error Disconnect(key string) error diff --git a/internal/core/domain/entity.go b/internal/core/domain/entity.go index 8341fde..0ee6125 100644 --- a/internal/core/domain/entity.go +++ b/internal/core/domain/entity.go @@ -2,8 +2,10 @@ package domain +import "udap/internal/core/domain/common" + type Entity struct { - Persistent + common.Persistent Name string `gorm:"unique" json:"name"` // Given name from module Alias string `json:"alias"` // Name from users Type string `json:"type"` // Type of entity {Light, Sensor, Etc} @@ -18,16 +20,12 @@ type Entity struct { } type EntityRepository interface { - FindAll() (*[]Entity, error) - FindById(id string) (*Entity, error) - Create(*Entity) error - FindOrCreate(*Entity) error + common.Persist[Entity] Register(*Entity) error - Update(*Entity) error - Delete(*Entity) error } type EntityService interface { + Observable FindAll() (*[]Entity, error) FindById(id string) (*Entity, error) Create(*Entity) error diff --git a/internal/core/domain/module.go b/internal/core/domain/module.go index 4da2d43..2b28ac8 100644 --- a/internal/core/domain/module.go +++ b/internal/core/domain/module.go @@ -2,8 +2,10 @@ package domain +import "udap/internal/core/domain/common" + type Module struct { - Persistent + common.Persistent Name string `json:"name"` Path string `json:"path"` Type string `json:"type"` @@ -17,13 +19,8 @@ type Module struct { } type ModuleRepository interface { - FindAll() (*[]Module, error) + common.Persist[Module] FindByName(name string) (*Module, error) - FindById(id string) (*Module, error) - Create(*Module) error - FindOrCreate(*Module) error - Update(*Module) error - Delete(*Module) error } type ModuleOperator interface { @@ -34,6 +31,7 @@ type ModuleOperator interface { } type ModuleService interface { + Observable Discover() error Build(module *Module) error Load(module *Module) error diff --git a/internal/core/domain/mutation.go b/internal/core/domain/mutation.go new file mode 100644 index 0000000..4a9e2df --- /dev/null +++ b/internal/core/domain/mutation.go @@ -0,0 +1,22 @@ +// Copyright (c) 2022 Braden Nicholson + +package domain + +type Mutation struct { + Status string `json:"status"` + Operation string `json:"operation"` + Body any `json:"body"` + Id string `json:"id"` +} + +type Observer struct { +} + +func (o *Observer) emit() { + +} + +type Observable interface { + Watch(chan<- Mutation) error + EmitAll() error +} diff --git a/internal/core/domain/network.go b/internal/core/domain/network.go index 8e95834..41c392f 100644 --- a/internal/core/domain/network.go +++ b/internal/core/domain/network.go @@ -2,8 +2,10 @@ package domain +import "udap/internal/core/domain/common" + type Network struct { - Persistent + common.Persistent Name string `json:"name"` Dns string `json:"dns"` Router string `json:"index"` @@ -13,18 +15,14 @@ type Network struct { } type NetworkRepository interface { - FindAll() ([]*Network, error) - FindById(id string) (*Network, error) + common.Persist[Network] FindByName(name string) (*Network, error) - Create(*Network) error Register(*Network) error - FindOrCreate(*Network) error - Update(*Network) error - Delete(*Network) error } type NetworkService interface { - FindAll() ([]*Network, error) + Observable + FindAll() (*[]Network, error) FindById(id string) (*Network, error) Create(*Network) error FindOrCreate(*Network) error diff --git a/internal/core/domain/user.go b/internal/core/domain/user.go index ea8e273..7e2badd 100644 --- a/internal/core/domain/user.go +++ b/internal/core/domain/user.go @@ -2,8 +2,10 @@ package domain +import "udap/internal/core/domain/common" + type User struct { - Persistent + common.Persistent Username string `json:"username"` First string `json:"first"` Middle string `json:"middle"` @@ -13,18 +15,14 @@ type User struct { } type UserRepository interface { - FindAll() ([]*User, error) - FindById(id string) (*User, error) - Create(*User) error - FindOrCreate(*User) error - Update(*User) error - Delete(*User) error + common.Persist[User] } type UserService interface { + Observable Register(*User) error Authenticate(*User) error - FindAll() ([]*User, error) + FindAll() (*[]User, error) FindById(id string) (*User, error) Create(*User) error FindOrCreate(*User) error diff --git a/internal/core/domain/zone.go b/internal/core/domain/zone.go index 9692ba8..7af12d5 100644 --- a/internal/core/domain/zone.go +++ b/internal/core/domain/zone.go @@ -2,24 +2,22 @@ package domain +import "udap/internal/core/domain/common" + type Zone struct { - Persistent + common.Persistent Name string `json:"name"` Entities []Entity `json:"entities" gorm:"many2many:zone_entities;"` User string `json:"user"` } type ZoneRepository interface { - FindAll() ([]*Zone, error) - FindById(id string) (*Zone, error) - Create(*Zone) error - FindOrCreate(*Zone) error - Update(*Zone) error - Delete(*Zone) error + common.Persist[Zone] } type ZoneService interface { - FindAll() ([]*Zone, error) + Observable + FindAll() (*[]Zone, error) FindById(id string) (*Zone, error) Create(*Zone) error FindOrCreate(*Zone) error diff --git a/internal/modules/attribute/attribute.go b/internal/core/modules/attribute/attribute.go similarity index 100% rename from internal/modules/attribute/attribute.go rename to internal/core/modules/attribute/attribute.go diff --git a/internal/modules/attribute/operator.go b/internal/core/modules/attribute/operator.go similarity index 73% rename from internal/modules/attribute/operator.go rename to internal/core/modules/attribute/operator.go index 86c228e..5c12a17 100644 --- a/internal/modules/attribute/operator.go +++ b/internal/core/modules/attribute/operator.go @@ -18,12 +18,15 @@ func NewOperator() domain.AttributeOperator { } } -func (a attributeOperator) Register(attribute *domain.Attribute) error { +func (a *attributeOperator) Register(attribute *domain.Attribute) error { + if attribute.Id == "" { + return fmt.Errorf("invalid attribute id") + } a.hooks[attribute.Id] = attribute.Channel return nil } -func (a attributeOperator) Request(attribute *domain.Attribute, s string) error { +func (a *attributeOperator) Request(attribute *domain.Attribute, s string) error { err := a.Set(attribute, s) if err != nil { return err @@ -31,7 +34,7 @@ func (a attributeOperator) Request(attribute *domain.Attribute, s string) error return nil } -func (a attributeOperator) Set(attribute *domain.Attribute, s string) error { +func (a *attributeOperator) Set(attribute *domain.Attribute, s string) error { // If the attribute handler is not set, return an error channel := a.hooks[attribute.Id] @@ -42,7 +45,7 @@ func (a attributeOperator) Set(attribute *domain.Attribute, s string) error { attribute.Requested = time.Now() if channel == nil { - return fmt.Errorf("channel is not open") + return nil } channel <- *attribute @@ -50,7 +53,7 @@ func (a attributeOperator) Set(attribute *domain.Attribute, s string) error { return nil } -func (a attributeOperator) Update(attribute *domain.Attribute, val string, stamp time.Time) error { +func (a *attributeOperator) Update(attribute *domain.Attribute, val string, stamp time.Time) error { // If a request has been made in the last five seconds, and has been unresolved, ignore this update if attribute.Requested.Before(stamp) && attribute.Request != val && time.Since(attribute.Requested) < 5*time.Second { return fmt.Errorf("OVERWRITES REQUEST") diff --git a/internal/modules/attribute/repository.go b/internal/core/modules/attribute/repository.go similarity index 71% rename from internal/modules/attribute/repository.go rename to internal/core/modules/attribute/repository.go index 3f177f3..932de36 100644 --- a/internal/modules/attribute/repository.go +++ b/internal/core/modules/attribute/repository.go @@ -17,7 +17,7 @@ func NewRepository(db *gorm.DB) domain.AttributeRepository { } } -func (u attributeRepo) Register(attribute *domain.Attribute) error { +func (u *attributeRepo) Register(attribute *domain.Attribute) error { if attribute.Id == "" { err := u.db.Model(&domain.Attribute{}).Where("entity = ? AND key = ?", attribute.Entity, attribute.Key).FirstOrCreate(attribute).Error @@ -37,7 +37,7 @@ func (u attributeRepo) Register(attribute *domain.Attribute) error { return nil } -func (u attributeRepo) FindByComposite(entity string, key string) (*domain.Attribute, error) { +func (u *attributeRepo) FindByComposite(entity string, key string) (*domain.Attribute, error) { var target domain.Attribute if err := u.db.Model(&domain.Attribute{}).Where("entity = ? AND key = ?", entity, key).First(&target).Error; err != nil { @@ -46,7 +46,7 @@ func (u attributeRepo) FindByComposite(entity string, key string) (*domain.Attri return &target, nil } -func (u attributeRepo) FindAllByEntity(entity string) (*[]domain.Attribute, error) { +func (u *attributeRepo) FindAllByEntity(entity string) (*[]domain.Attribute, error) { var target *[]domain.Attribute if err := u.db.Where("entity = ?", entity).Find(target).Error; err != nil { return nil, err @@ -54,7 +54,7 @@ func (u attributeRepo) FindAllByEntity(entity string) (*[]domain.Attribute, erro return target, nil } -func (u attributeRepo) FindAll() (*[]domain.Attribute, error) { +func (u *attributeRepo) FindAll() (*[]domain.Attribute, error) { var target []domain.Attribute if err := u.db.First(&target).Error; err != nil { return nil, err @@ -62,7 +62,7 @@ func (u attributeRepo) FindAll() (*[]domain.Attribute, error) { return &target, nil } -func (u attributeRepo) FindById(id string) (*domain.Attribute, error) { +func (u *attributeRepo) FindById(id string) (*domain.Attribute, error) { var target domain.Attribute if err := u.db.Where("id = ?", id).First(&target).Error; err != nil { return nil, err @@ -70,28 +70,28 @@ func (u attributeRepo) FindById(id string) (*domain.Attribute, error) { return &target, nil } -func (u attributeRepo) Create(attribute *domain.Attribute) error { +func (u *attributeRepo) Create(attribute *domain.Attribute) error { if err := u.db.Create(attribute).Error; err != nil { return err } return nil } -func (u attributeRepo) FindOrCreate(attribute *domain.Attribute) error { +func (u *attributeRepo) FindOrCreate(attribute *domain.Attribute) error { if err := u.db.FirstOrCreate(attribute).Error; err != nil { return err } return nil } -func (u attributeRepo) Update(attribute *domain.Attribute) error { +func (u *attributeRepo) Update(attribute *domain.Attribute) error { if err := u.db.Save(attribute).Error; err != nil { return err } return nil } -func (u attributeRepo) Delete(attribute *domain.Attribute) error { +func (u *attributeRepo) Delete(attribute *domain.Attribute) error { if err := u.db.Delete(attribute).Error; err != nil { return err } diff --git a/internal/modules/attribute/service.go b/internal/core/modules/attribute/service.go similarity index 65% rename from internal/modules/attribute/service.go rename to internal/core/modules/attribute/service.go index 21ba404..95b9b06 100644 --- a/internal/modules/attribute/service.go +++ b/internal/core/modules/attribute/service.go @@ -3,6 +3,7 @@ package attribute import ( + "fmt" "time" "udap/internal/core/domain" ) @@ -10,7 +11,7 @@ import ( type attributeService struct { repository domain.AttributeRepository operator domain.AttributeOperator - channel chan<- domain.Attribute + channel chan<- domain.Mutation } func (u *attributeService) EmitAll() error { @@ -18,9 +19,8 @@ func (u *attributeService) EmitAll() error { if err != nil { return err } - attributes := *all - for _, attribute := range attributes { - err = u.push(&attribute) + for _, attribute := range *all { + err = u.emit(&attribute) if err != nil { return err } @@ -28,17 +28,28 @@ func (u *attributeService) EmitAll() error { return nil } -func (u *attributeService) push(attribute *domain.Attribute) error { - u.channel <- *attribute +func (u *attributeService) emit(attribute *domain.Attribute) error { + if u.channel == nil { + return fmt.Errorf("channel is null") + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "attribute", + Body: *attribute, + Id: attribute.Id, + } return nil } -func (u *attributeService) Watch(channel chan<- domain.Attribute) error { - u.channel = channel +func (u *attributeService) Watch(ref chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel in use") + } + u.channel = ref return nil } -func (u attributeService) FindAllByEntity(entity string) (*[]domain.Attribute, error) { +func (u *attributeService) FindAllByEntity(entity string) (*[]domain.Attribute, error) { return u.repository.FindAllByEntity(entity) } @@ -50,7 +61,7 @@ func NewService(repository domain.AttributeRepository, operator domain.Attribute } } -func (u attributeService) Register(attribute *domain.Attribute) error { +func (u *attributeService) Register(attribute *domain.Attribute) error { err := u.repository.Register(attribute) if err != nil { return err @@ -59,11 +70,14 @@ func (u attributeService) Register(attribute *domain.Attribute) error { if err != nil { return err } - + err = u.emit(attribute) + if err != nil { + return err + } return nil } -func (u attributeService) Request(entity string, key string, value string) error { +func (u *attributeService) Request(entity string, key string, value string) error { e, err := u.repository.FindByComposite(entity, key) if err != nil { return err @@ -72,14 +86,14 @@ func (u attributeService) Request(entity string, key string, value string) error if err != nil { return err } - err = u.push(e) + err = u.emit(e) if err != nil { return err } return nil } -func (u attributeService) Set(entity string, key string, value string) error { +func (u *attributeService) Set(entity string, key string, value string) error { e, err := u.repository.FindByComposite(entity, key) if err != nil { return err @@ -88,14 +102,14 @@ func (u attributeService) Set(entity string, key string, value string) error { if err != nil { return err } - err = u.push(e) + err = u.emit(e) if err != nil { return err } return nil } -func (u attributeService) Update(entity string, key string, value string, stamp time.Time) error { +func (u *attributeService) Update(entity string, key string, value string, stamp time.Time) error { e, err := u.repository.FindByComposite(entity, key) if err != nil { return err @@ -104,7 +118,8 @@ func (u attributeService) Update(entity string, key string, value string, stamp if err != nil { return err } - err = u.push(e) + + err = u.emit(e) if err != nil { return err } diff --git a/internal/modules/device/device.go b/internal/core/modules/device/device.go similarity index 100% rename from internal/modules/device/device.go rename to internal/core/modules/device/device.go diff --git a/internal/modules/device/repository.go b/internal/core/modules/device/repository.go similarity index 86% rename from internal/modules/device/repository.go rename to internal/core/modules/device/repository.go index e7bfd76..4779fb5 100644 --- a/internal/modules/device/repository.go +++ b/internal/core/modules/device/repository.go @@ -17,12 +17,12 @@ func NewRepository(db *gorm.DB) domain.DeviceRepository { } } -func (u deviceRepo) FindAll() ([]*domain.Device, error) { - var target []*domain.Device - if err := u.db.First(target).Error; err != nil { +func (u deviceRepo) FindAll() (*[]domain.Device, error) { + var target []domain.Device + if err := u.db.First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u deviceRepo) FindById(id string) (*domain.Device, error) { diff --git a/internal/modules/device/service.go b/internal/core/modules/device/service.go similarity index 53% rename from internal/modules/device/service.go rename to internal/core/modules/device/service.go index 7b831eb..576a93c 100644 --- a/internal/modules/device/service.go +++ b/internal/core/modules/device/service.go @@ -3,11 +3,49 @@ package device import ( + "fmt" "udap/internal/core/domain" ) type deviceService struct { repository domain.DeviceRepository + channel chan<- domain.Mutation +} + +func (u *deviceService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + for _, device := range *all { + err = u.emit(&device) + if err != nil { + return err + } + } + return nil +} + +func (u *deviceService) emit(device *domain.Device) error { + if u.channel == nil { + return nil + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "device", + Body: *device, + Id: device.Id, + } + return nil +} + +func (u *deviceService) Watch(mut chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel already set") + } + u.channel = mut + + return nil } func (u deviceService) Register(device *domain.Device) error { @@ -15,12 +53,12 @@ func (u deviceService) Register(device *domain.Device) error { } func NewService(repository domain.DeviceRepository) domain.DeviceService { - return deviceService{repository: repository} + return &deviceService{repository: repository} } // Repository Mapping -func (u deviceService) FindAll() ([]*domain.Device, error) { +func (u deviceService) FindAll() (*[]domain.Device, error) { return u.repository.FindAll() } diff --git a/internal/modules/endpoint/endpoint.go b/internal/core/modules/endpoint/endpoint.go similarity index 57% rename from internal/modules/endpoint/endpoint.go rename to internal/core/modules/endpoint/endpoint.go index c096754..b854958 100644 --- a/internal/modules/endpoint/endpoint.go +++ b/internal/core/modules/endpoint/endpoint.go @@ -4,12 +4,13 @@ package endpoint import ( "gorm.io/gorm" + "udap/internal/controller" "udap/internal/core/domain" ) -func New(db *gorm.DB) domain.EndpointService { +func New(db *gorm.DB, controller *controller.Controller) domain.EndpointService { repo := NewRepository(db) - operator := NewOperator() + operator := NewOperator(controller) service := NewService(repo, operator) return service } diff --git a/internal/core/modules/endpoint/operator.go b/internal/core/modules/endpoint/operator.go new file mode 100644 index 0000000..ee7ca93 --- /dev/null +++ b/internal/core/modules/endpoint/operator.go @@ -0,0 +1,194 @@ +// Copyright (c) 2022 Braden Nicholson + +package endpoint + +import ( + "fmt" + "github.com/gorilla/websocket" + "sync" + "udap/internal/controller" + "udap/internal/core/domain" + "udap/internal/log" +) + +type Connection struct { + WS *websocket.Conn + active *bool + edit chan any + done chan bool +} + +func (c *Connection) Active() bool { + return *c.active +} + +func (c *Connection) Send(body any) { + if c.Active() && c.edit != nil { + c.edit <- body + } +} + +func NewConnection(ws *websocket.Conn) *Connection { + ch := make(chan any, 8) + d := make(chan bool) + a := true + c := &Connection{ + WS: ws, + edit: ch, + done: d, + active: &a, + } + return c +} + +func (c *Connection) Close() { + if c.edit != nil { + return + } + err := c.WS.Close() + if err != nil { + return + } + close(c.edit) + close(c.done) + a := false + c.active = &a +} + +func (c *Connection) Watch() { + for a := range c.edit { + if c.WS == nil { + return + } + err := c.WS.WriteJSON(a) + if err != nil { + log.Err(err) + continue + } + } +} + +type endpointOperator struct { + connections map[string]*Connection + controller *controller.Controller +} + +func (m *endpointOperator) getConnection(id string) (*Connection, error) { + ref := m.connections[id] + if ref == nil { + return nil, fmt.Errorf("connection not found") + } + return ref, nil +} + +func (m *endpointOperator) setConnection(id string, connection *Connection) error { + m.connections[id] = connection + return nil +} + +func (m *endpointOperator) removeConnection(id string) error { + ref := m.connections[id] + if ref != nil { + return nil + } + delete(m.connections, id) + + return nil +} + +func (m *endpointOperator) SendAll(id string, operation string, payload any) error { + for _, conn := range m.connections { + if conn.Active() { + conn.Send(Response{ + Id: id, + Status: "success", + Operation: operation, + Body: payload, + }) + } + } + return nil +} + +func (m *endpointOperator) Send(id string, operation string, payload any) error { + connection, err := m.getConnection(id) + if err != nil { + return err + } + connection.Send(Response{ + Status: "success", + Operation: operation, + Body: payload, + }) + return nil +} + +type Metadata struct { + System System `json:"system"` +} + +type Response struct { + Id string `json:"id"` + Status string `json:"status"` + Operation string `json:"operation"` + Body any `json:"body"` +} + +func (m *endpointOperator) Enroll(endpoint *domain.Endpoint, conn *websocket.Conn) error { + + connection := NewConnection(conn) + err := m.setConnection(endpoint.Id, connection) + if err != nil { + return err + } + + info, err := systemInfo() + if err != nil { + return err + } + + connection.Send(Response{ + Id: "", + Status: "success", + Operation: "metadata", + Body: Metadata{System: info}, + }) + if err != nil { + return err + } + + log.Event("Endpoint '%s' connected.", endpoint.Name) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + connection.Watch() + }() + err = m.controller.EmitAll() + if err != nil { + return err + } + wg.Wait() + return nil +} + +func (m *endpointOperator) Unenroll(id string) error { + connection, err := m.getConnection(id) + if err != nil { + return err + } + connection.Close() + log.Event("Endpoint '%s' disconnected.", id) + err = m.removeConnection(id) + if err != nil { + return err + } + return nil +} + +func NewOperator(controller *controller.Controller) domain.EndpointOperator { + return &endpointOperator{ + controller: controller, + connections: map[string]*Connection{}, + } +} diff --git a/internal/modules/endpoint/repository.go b/internal/core/modules/endpoint/repository.go similarity index 100% rename from internal/modules/endpoint/repository.go rename to internal/core/modules/endpoint/repository.go diff --git a/internal/modules/endpoint/service.go b/internal/core/modules/endpoint/service.go similarity index 55% rename from internal/modules/endpoint/service.go rename to internal/core/modules/endpoint/service.go index 4b02425..9e82340 100644 --- a/internal/modules/endpoint/service.go +++ b/internal/core/modules/endpoint/service.go @@ -3,6 +3,7 @@ package endpoint import ( + "fmt" "github.com/gorilla/websocket" "udap/internal/core/domain" ) @@ -10,6 +11,50 @@ import ( type endpointService struct { repository domain.EndpointRepository operator domain.EndpointOperator + channel chan<- domain.Mutation +} + +func (u *endpointService) SendAll(target string, operation string, payload any) error { + err := u.operator.SendAll(target, operation, payload) + if err != nil { + return err + } + return nil +} + +func (u *endpointService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + for _, endpoint := range *all { + err = u.emit(&endpoint) + if err != nil { + return err + } + } + return nil +} + +func (u *endpointService) emit(endpoint *domain.Endpoint) error { + if u.channel == nil { + return fmt.Errorf("channel is null") + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "endpoint", + Body: *endpoint, + Id: endpoint.Id, + } + return nil +} + +func (u *endpointService) Watch(ref chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel in use") + } + u.channel = ref + return nil } func (u endpointService) Send(id string, operation string, payload any) error { @@ -24,7 +69,7 @@ func (u endpointService) FindByKey(key string) (*domain.Endpoint, error) { return u.repository.FindByKey(key) } -func (u endpointService) Enroll(id string, conn *websocket.Conn) error { +func (u *endpointService) Enroll(id string, conn *websocket.Conn) error { endpoint, err := u.FindById(id) if err != nil { return err @@ -36,13 +81,16 @@ func (u endpointService) Enroll(id string, conn *websocket.Conn) error { return nil } -func (u endpointService) Disconnect(key string) error { - // TODO implement me - panic("implement me") +func (u *endpointService) Disconnect(key string) error { + err := u.operator.Unenroll(key) + if err != nil { + return err + } + return nil } func NewService(repository domain.EndpointRepository, operator domain.EndpointOperator) domain.EndpointService { - return endpointService{repository: repository, operator: operator} + return &endpointService{repository: repository, operator: operator} } // Repository Mapping diff --git a/internal/modules/endpoint/system.go b/internal/core/modules/endpoint/system.go similarity index 100% rename from internal/modules/endpoint/system.go rename to internal/core/modules/endpoint/system.go diff --git a/internal/modules/entity/entity.go b/internal/core/modules/entity/entity.go similarity index 100% rename from internal/modules/entity/entity.go rename to internal/core/modules/entity/entity.go diff --git a/internal/modules/entity/repository.go b/internal/core/modules/entity/repository.go similarity index 95% rename from internal/modules/entity/repository.go rename to internal/core/modules/entity/repository.go index 7f4131d..8e6e0c2 100644 --- a/internal/modules/entity/repository.go +++ b/internal/core/modules/entity/repository.go @@ -13,17 +13,17 @@ type entityRepo struct { func (u entityRepo) Register(e *domain.Entity) error { if e.Id == "" { - err := u.db.Model(&domain.Entity{}).Where("name = ? AND module = ?", e.Name, e.Module).FirstOrCreate(e).Error + err := u.db.Model(&domain.Entity{}).Where("name = ? AND module = ?", e.Name, e.Module).FirstOrCreate(&e).Error if err != nil { return err } } else { - err := u.db.Model(&domain.Entity{}).Where("name = ?", e.Name).First(e).Error + err := u.db.Model(&domain.Entity{}).Where("name = ?", e.Name).First(&e).Error if err != nil { return err } } - err := u.db.Model(&domain.Entity{}).Where("name = ?", e.Name).Save(e).Error + err := u.db.Model(&domain.Entity{}).Where("name = ?", e.Name).Save(&e).Error if err != nil { return err } diff --git a/internal/modules/entity/service.go b/internal/core/modules/entity/service.go similarity index 61% rename from internal/modules/entity/service.go rename to internal/core/modules/entity/service.go index af9fb79..fcd777d 100644 --- a/internal/modules/entity/service.go +++ b/internal/core/modules/entity/service.go @@ -3,12 +3,50 @@ package entity import ( + "fmt" "udap/internal/core/domain" "udap/internal/log" ) type entityService struct { repository domain.EntityRepository + channel chan<- domain.Mutation +} + +func (u *entityService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + for _, entity := range *all { + err = u.emit(&entity) + if err != nil { + return err + } + } + return nil +} + +func (u *entityService) emit(entity *domain.Entity) error { + if u.channel == nil { + return nil + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "entity", + Body: *entity, + Id: entity.Id, + } + return nil +} + +func (u *entityService) Watch(mut chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel already set") + } + u.channel = mut + + return nil } func (u entityService) Config(id string, value string) error { @@ -21,6 +59,10 @@ func (u entityService) Config(id string, value string) error { if err != nil { return err } + err = u.emit(entity) + if err != nil { + return err + } return nil } @@ -30,11 +72,15 @@ func (u entityService) Register(entity *domain.Entity) error { return err } log.Event("Entity '%s' registered.", entity.Name) + err = u.emit(entity) + if err != nil { + return err + } return nil } func NewService(repository domain.EntityRepository) domain.EntityService { - return entityService{repository: repository} + return &entityService{repository: repository} } // Repository Mapping diff --git a/internal/modules/module/module.go b/internal/core/modules/module/module.go similarity index 100% rename from internal/modules/module/module.go rename to internal/core/modules/module/module.go diff --git a/internal/modules/module/operator.go b/internal/core/modules/module/operator.go similarity index 80% rename from internal/modules/module/operator.go rename to internal/core/modules/module/operator.go index 0f12fa8..05a91b1 100644 --- a/internal/modules/module/operator.go +++ b/internal/core/modules/module/operator.go @@ -13,6 +13,7 @@ import ( "udap/internal/controller" "udap/internal/core/domain" "udap/internal/log" + "udap/internal/pulse" "udap/pkg/plugin" ) @@ -21,15 +22,27 @@ type moduleOperator struct { modules map[string]plugin.ModuleInterface } -func (m moduleOperator) Update(module *domain.Module) error { +func NewOperator(ctrl *controller.Controller) domain.ModuleOperator { + return &moduleOperator{ + ctrl: ctrl, + modules: map[string]plugin.ModuleInterface{}, + } +} + +func (m *moduleOperator) Update(module *domain.Module) error { + if m.modules[module.Name] == nil { + return fmt.Errorf("nothing to update") + } + pulse.Begin(module.Id) err := m.modules[module.Name].Update() + pulse.End(module.Id) if err != nil { return err } return nil } -func (m moduleOperator) Run(module *domain.Module) error { +func (m *moduleOperator) Run(module *domain.Module) error { err := m.modules[module.Name].Run() if err != nil { return err @@ -37,14 +50,7 @@ func (m moduleOperator) Run(module *domain.Module) error { return nil } -func NewOperator(ctrl *controller.Controller) domain.ModuleOperator { - return &moduleOperator{ - ctrl: ctrl, - modules: map[string]plugin.ModuleInterface{}, - } -} - -func (m moduleOperator) Build(module *domain.Module) error { +func (m *moduleOperator) Build(module *domain.Module) error { start := time.Now() if _, err := os.Stat(module.Path); err != nil { return err @@ -64,11 +70,11 @@ func (m moduleOperator) Build(module *domain.Module) error { log.ErrF(errors.New(string(output)), "Module '%s' build failed:", module.Name) return nil } - log.Event("Module '%s' compiled successfully (%s)", module.Name, time.Since(start).Truncate(time.Millisecond).String()) + log.Event("Module '%s' compiled. (%s)", module.Name, time.Since(start).Truncate(time.Millisecond).String()) return nil } -func (m moduleOperator) Load(module *domain.Module) error { +func (m *moduleOperator) Load(module *domain.Module) error { binary := strings.Replace(module.Path, ".go", ".so", 1) p, err := plugin.Load(binary) if err != nil { diff --git a/internal/modules/module/repository.go b/internal/core/modules/module/repository.go similarity index 100% rename from internal/modules/module/repository.go rename to internal/core/modules/module/repository.go diff --git a/internal/modules/module/service.go b/internal/core/modules/module/service.go similarity index 81% rename from internal/modules/module/service.go rename to internal/core/modules/module/service.go index 55236e7..da4b5f6 100644 --- a/internal/modules/module/service.go +++ b/internal/core/modules/module/service.go @@ -16,6 +16,42 @@ const DIR = "modules" type moduleService struct { repository domain.ModuleRepository operator domain.ModuleOperator + channel chan<- domain.Mutation +} + +func (u *moduleService) Watch(ref chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel in use") + } + u.channel = ref + return nil +} + +func (u *moduleService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + for _, module := range *all { + err = u.emit(&module) + if err != nil { + return err + } + } + return nil +} + +func (u *moduleService) emit(module *domain.Module) error { + if u.channel == nil { + return fmt.Errorf("channel is null") + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "module", + Body: *module, + Id: module.Id, + } + return nil } func (u *moduleService) Update(module *domain.Module) error { @@ -52,9 +88,12 @@ func (u *moduleService) UpdateAll() error { for _, module := range *modules { go func(mod domain.Module) { defer wg.Done() - err = u.Update(&mod) - if err != nil { - log.Err(err) + if mod.Enabled { + err = u.Update(&mod) + if err != nil { + log.Err(err) + return + } } }(module) } @@ -121,6 +160,7 @@ func (u moduleService) Discover() error { var target *domain.Module target, err = u.repository.FindByName(name) if err != nil { + target = &domain.Module{} target.Name = name target.Path = p err = u.repository.Create(target) diff --git a/internal/modules/network/network.go b/internal/core/modules/network/network.go similarity index 100% rename from internal/modules/network/network.go rename to internal/core/modules/network/network.go diff --git a/internal/modules/network/repository.go b/internal/core/modules/network/repository.go similarity index 85% rename from internal/modules/network/repository.go rename to internal/core/modules/network/repository.go index cf4b617..1e7acc1 100644 --- a/internal/modules/network/repository.go +++ b/internal/core/modules/network/repository.go @@ -31,11 +31,11 @@ func (u networkRepo) Register(network *domain.Network) error { } func (u networkRepo) FindByName(name string) (*domain.Network, error) { - var target *domain.Network - if err := u.db.Where("name = ?", name).Find(target).Error; err != nil { + var target domain.Network + if err := u.db.Where("name = ?", name).Find(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func NewRepository(db *gorm.DB) domain.NetworkRepository { @@ -44,12 +44,12 @@ func NewRepository(db *gorm.DB) domain.NetworkRepository { } } -func (u networkRepo) FindAll() ([]*domain.Network, error) { - var target []*domain.Network - if err := u.db.First(target).Error; err != nil { +func (u networkRepo) FindAll() (*[]domain.Network, error) { + var target []domain.Network + if err := u.db.First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u networkRepo) FindById(id string) (*domain.Network, error) { diff --git a/internal/modules/network/service.go b/internal/core/modules/network/service.go similarity index 54% rename from internal/modules/network/service.go rename to internal/core/modules/network/service.go index f589a24..6e5f951 100644 --- a/internal/modules/network/service.go +++ b/internal/core/modules/network/service.go @@ -3,11 +3,49 @@ package network import ( + "fmt" "udap/internal/core/domain" ) type networkService struct { repository domain.NetworkRepository + channel chan<- domain.Mutation +} + +func (u *networkService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + for _, network := range *all { + err = u.emit(&network) + if err != nil { + return err + } + } + return nil +} + +func (u *networkService) emit(network *domain.Network) error { + if u.channel == nil { + return nil + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "network", + Body: *network, + Id: network.Id, + } + return nil +} + +func (u *networkService) Watch(mut chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel already set") + } + u.channel = mut + + return nil } func (u networkService) Register(network *domain.Network) error { @@ -15,12 +53,12 @@ func (u networkService) Register(network *domain.Network) error { } func NewService(repository domain.NetworkRepository) domain.NetworkService { - return networkService{repository: repository} + return &networkService{repository: repository} } // Repository Mapping -func (u networkService) FindAll() ([]*domain.Network, error) { +func (u networkService) FindAll() (*[]domain.Network, error) { return u.repository.FindAll() } diff --git a/internal/modules/user/repository.go b/internal/core/modules/user/repository.go similarity index 86% rename from internal/modules/user/repository.go rename to internal/core/modules/user/repository.go index e26cf08..554538f 100644 --- a/internal/modules/user/repository.go +++ b/internal/core/modules/user/repository.go @@ -17,12 +17,12 @@ func NewRepository(db *gorm.DB) domain.UserRepository { } } -func (u userRepo) FindAll() ([]*domain.User, error) { - var target []*domain.User - if err := u.db.First(target).Error; err != nil { +func (u userRepo) FindAll() (*[]domain.User, error) { + var target []domain.User + if err := u.db.Find(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u userRepo) FindById(id string) (*domain.User, error) { diff --git a/internal/modules/user/service.go b/internal/core/modules/user/service.go similarity index 68% rename from internal/modules/user/service.go rename to internal/core/modules/user/service.go index 3080f67..c2937e9 100644 --- a/internal/modules/user/service.go +++ b/internal/core/modules/user/service.go @@ -10,10 +10,47 @@ import ( type userService struct { repository domain.UserRepository + channel chan<- domain.Mutation +} + +func (u *userService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + for _, user := range *all { + err = u.emit(&user) + if err != nil { + return err + } + } + return nil +} + +func (u *userService) emit(user *domain.User) error { + if u.channel == nil { + return nil + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "user", + Body: *user, + Id: user.Id, + } + return nil +} + +func (u *userService) Watch(mut chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel already set") + } + u.channel = mut + + return nil } func NewService(repository domain.UserRepository) domain.UserService { - return userService{repository: repository} + return &userService{repository: repository} } // Services @@ -55,7 +92,7 @@ func CheckPasswordHash(password, hash string) bool { // Repository Mapping -func (u userService) FindAll() ([]*domain.User, error) { +func (u userService) FindAll() (*[]domain.User, error) { return u.repository.FindAll() } diff --git a/internal/modules/user/user.go b/internal/core/modules/user/user.go similarity index 100% rename from internal/modules/user/user.go rename to internal/core/modules/user/user.go diff --git a/internal/modules/zone/repository.go b/internal/core/modules/zone/repository.go similarity index 86% rename from internal/modules/zone/repository.go rename to internal/core/modules/zone/repository.go index a445f90..036c1d1 100644 --- a/internal/modules/zone/repository.go +++ b/internal/core/modules/zone/repository.go @@ -17,12 +17,12 @@ func NewRepository(db *gorm.DB) domain.ZoneRepository { } } -func (u zoneRepo) FindAll() ([]*domain.Zone, error) { - var target []*domain.Zone - if err := u.db.First(target).Error; err != nil { +func (u zoneRepo) FindAll() (*[]domain.Zone, error) { + var target []domain.Zone + if err := u.db.First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u zoneRepo) FindById(id string) (*domain.Zone, error) { diff --git a/internal/modules/zone/service.go b/internal/core/modules/zone/service.go similarity index 50% rename from internal/modules/zone/service.go rename to internal/core/modules/zone/service.go index 972844c..174a154 100644 --- a/internal/modules/zone/service.go +++ b/internal/core/modules/zone/service.go @@ -3,20 +3,58 @@ package zone import ( + "fmt" "udap/internal/core/domain" ) type zoneService struct { repository domain.ZoneRepository + channel chan<- domain.Mutation +} + +func (u *zoneService) EmitAll() error { + all, err := u.FindAll() + if err != nil { + return err + } + for _, zone := range *all { + err = u.emit(&zone) + if err != nil { + return err + } + } + return nil +} + +func (u *zoneService) emit(zone *domain.Zone) error { + if u.channel == nil { + return nil + } + u.channel <- domain.Mutation{ + Status: "update", + Operation: "zone", + Body: *zone, + Id: zone.Id, + } + return nil +} + +func (u *zoneService) Watch(mut chan<- domain.Mutation) error { + if u.channel != nil { + return fmt.Errorf("channel already set") + } + u.channel = mut + + return nil } func NewService(repository domain.ZoneRepository) domain.ZoneService { - return zoneService{repository: repository} + return &zoneService{repository: repository} } // Repository Mapping -func (u zoneService) FindAll() ([]*domain.Zone, error) { +func (u zoneService) FindAll() (*[]domain.Zone, error) { return u.repository.FindAll() } diff --git a/internal/modules/zone/zone.go b/internal/core/modules/zone/zone.go similarity index 100% rename from internal/modules/zone/zone.go rename to internal/core/modules/zone/zone.go diff --git a/internal/log/log.go b/internal/log/log.go index 4eb1866..bb86076 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -130,7 +130,9 @@ func ErrF(err error, format string, args ...interface{}) { func Err(err error) { _, file, ln, ok := runtime.Caller(1) if ok { - fmt.Printf("%s%s%s %s%s\n", Reset+BoldRed, fmt.Sprintf("Error (%s:%d)", filepath.Base(file), ln), + fmt.Printf("%s%s%s %s%s\n", + Reset+BoldRed, + fmt.Sprintf("[ERR*] (%s:%d)", filepath.Base(file), ln), Reset+FaintRed, fmt.Sprintf(err.Error()), Reset) } diff --git a/internal/modules/endpoint/operator.go b/internal/modules/endpoint/operator.go deleted file mode 100644 index d1b753d..0000000 --- a/internal/modules/endpoint/operator.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2022 Braden Nicholson - -package endpoint - -import ( - "github.com/gorilla/websocket" - "udap/internal/core/domain" - "udap/internal/log" -) - -type endpointOperator struct { - connections map[string]*websocket.Conn -} - -func (m *endpointOperator) Send(id string, operation string, payload any) error { - if m.connections[id] == nil { - return nil - } - err := m.connections[id].WriteJSON(Response{ - Status: "success", - Operation: operation, - Body: payload, - }) - if err != nil { - return err - } - return nil -} - -type Metadata struct { - System System `json:"system"` -} - -type Response struct { - Id string `json:"id"` - Status string `json:"status"` - Operation string `json:"operation"` - Body any `json:"body"` -} - -func (m *endpointOperator) Enroll(endpoint *domain.Endpoint, conn *websocket.Conn) error { - m.connections[endpoint.Id] = conn - m.connections[endpoint.Id].SetCloseHandler(func(code int, text string) error { - log.Event("Endpoint '%s' disconnected.", endpoint.Name) - return nil - }) - info, err := systemInfo() - if err != nil { - return err - } - err = conn.WriteJSON(Response{ - Id: "", - Status: "success", - Operation: "metadata", - Body: Metadata{System: info}, - }) - if err != nil { - return err - } - - log.Event("Endpoint '%s' connected.", endpoint.Name) - return nil -} - -func NewOperator() domain.EndpointOperator { - return &endpointOperator{ - connections: map[string]*websocket.Conn{}, - } -} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index a9df96c..bf5f4e7 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -10,8 +10,9 @@ import ( "udap/internal/controller" "udap/internal/core" "udap/internal/core/domain" + "udap/internal/core/modules/endpoint" + "udap/internal/core/modules/module" "udap/internal/log" - "udap/internal/modules/module" "udap/internal/port/routes" "udap/internal/port/runtimes" "udap/internal/pulse" @@ -26,91 +27,8 @@ type orchestrator struct { controller *controller.Controller - modules domain.ModuleService -} - -func (o *orchestrator) Update() error { - endpoints, err := o.controller.Endpoints.FindAll() - if err != nil { - return err - } - eps := *endpoints - for i := range eps { - ep := eps[i] - err = o.controller.Endpoints.Send(ep.Id, "endpoint", ep) - if err != nil { - return nil - } - } - err = o.controller.Attributes.EmitAll() - if err != nil { - return err - } - return nil -} - -func (o *orchestrator) Timings() error { - timings := pulse.Timings.Timings() - for _, timing := range timings { - err := o.controller.Endpoints.Send("", "timing", timing) - if err != nil { - return err - } - } - return nil -} - -func (o *orchestrator) Run() error { - - o.server = &http.Server{Addr: ":3020", Handler: o.router} - - go func() { - err := o.server.ListenAndServe() - if err != nil { - log.ErrF(err, "http server exited with error:\n") - } - }() - - attrs := make(chan domain.Attribute) - err := o.controller.Attributes.Watch(attrs) - if err != nil { - return err - } - go func() { - for attr := range attrs { - err = o.controller.Endpoints.Send("", "attribute", attr) - if err != nil { - return - } - } - }() - - delay := 1000.0 - for { - select { - case <-time.After(time.Millisecond * time.Duration(delay)): - log.Event("Update timed out") - default: - start := time.Now() - err := o.Update() - if err != nil { - log.ErrF(err, "runtime update error: %s") - } - d := time.Since(start) - dur := (time.Millisecond * time.Duration(delay)) - d - if dur > 0 { - time.Sleep(dur) - } - } - - err := o.Timings() - if err != nil { - return err - } - - } - - return nil + modules domain.ModuleService + endpoints domain.EndpointService } type Orchestrator interface { @@ -135,9 +53,8 @@ func NewOrchestrator() Orchestrator { } func (o *orchestrator) Start() error { - var err error - err = core.MigrateModels(o.db) + err := core.MigrateModels(o.db) if err != nil { return err } @@ -148,13 +65,86 @@ func (o *orchestrator) Start() error { } o.modules = module.New(o.db, o.controller) + o.endpoints = endpoint.New(o.db, o.controller) + + o.controller.Endpoints = o.endpoints + o.controller.Modules = o.modules + + return nil +} + +func (o *orchestrator) Update() error { + err := o.modules.UpdateAll() + if err != nil { + return err + } + return nil +} + +func (o *orchestrator) Run() error { + + resp := make(chan domain.Mutation, 8) + o.controller.Listen(resp) + + go func() { + for response := range resp { + err := o.endpoints.SendAll(response.Id, response.Operation, response.Body) + if err != nil { + log.Err(err) + return + } + } + + }() // Initialize and route applicable domains routes.NewUserRouter(o.controller.Users).RouteUsers(o.router) - routes.NewEndpointRouter(o.controller.Endpoints).RouteEndpoints(o.router) + routes.NewEndpointRouter(o.endpoints).RouteEndpoints(o.router) routes.NewModuleRouter(o.modules).RouteModules(o.router) runtimes.NewModuleRuntime(o.modules) - return nil + o.server = &http.Server{Addr: ":3020", Handler: o.router} + + go func() { + err := o.server.ListenAndServe() + if err != nil { + log.ErrF(err, "http server exited with error:\n") + } + }() + + delay := 1000.0 + for { + pulse.Begin("update") + select { + case <-time.After(time.Millisecond * time.Duration(delay)): + log.Event("Orchestrator event loop timed out") + continue + default: + start := time.Now() + + err := o.Update() + if err != nil { + log.ErrF(err, "runtime update error: %s") + } + + delta := time.Since(start) + duration := (time.Millisecond * time.Duration(delay)) - delta + if duration > 0 { + log.Event("Tick Complete (%s)", delta) + time.Sleep(duration) + } + + } + pulse.End("update") + timings := pulse.Timings.Timings() + for s, proc := range timings { + resp <- domain.Mutation{ + Status: "update", + Operation: "timing", + Body: proc, + Id: s, + } + } + } } diff --git a/internal/port/routes/endpoint.go b/internal/port/routes/endpoint.go index 571808c..4d5d2ac 100644 --- a/internal/port/routes/endpoint.go +++ b/internal/port/routes/endpoint.go @@ -21,12 +21,12 @@ type endpointRouter struct { } func NewEndpointRouter(service domain.EndpointService) EndpointRouter { - return endpointRouter{ + return &endpointRouter{ service: service, } } -func (r endpointRouter) RouteEndpoints(router chi.Router) { +func (r *endpointRouter) RouteEndpoints(router chi.Router) { router.Get("/socket/{token}", r.enroll) router.Route("/endpoints", func(local chi.Router) { local.Get("/register/{key}", r.authenticate) @@ -37,7 +37,7 @@ type authenticationResponse struct { Token string `json:"token"` } -func (r endpointRouter) authenticate(w http.ResponseWriter, req *http.Request) { +func (r *endpointRouter) authenticate(w http.ResponseWriter, req *http.Request) { key := chi.URLParam(req, "key") if key == "" { http.Error(w, "access key not provided", 401) @@ -72,7 +72,7 @@ func (r endpointRouter) authenticate(w http.ResponseWriter, req *http.Request) { w.WriteHeader(200) } -func (r endpointRouter) enroll(w http.ResponseWriter, req *http.Request) { +func (r *endpointRouter) enroll(w http.ResponseWriter, req *http.Request) { // Initialize an error to manage returns var err error // Convert the basic GET request into a WebSocket session @@ -103,4 +103,8 @@ func (r endpointRouter) enroll(w http.ResponseWriter, req *http.Request) { return } + err = r.service.Disconnect(id) + if err != nil { + return + } } diff --git a/modules/atlas/atlas.go b/modules/atlas/atlas.go new file mode 100644 index 0000000..ce8e6d7 --- /dev/null +++ b/modules/atlas/atlas.go @@ -0,0 +1,177 @@ +// Copyright (c) 2021 Braden Nicholson + +package main + +import ( + "context" + "fmt" + "github.com/gorilla/websocket" + "net/url" + "os" + "os/exec" + "strings" + "time" + "udap/internal/core/domain" + "udap/internal/log" + "udap/pkg/plugin" +) + +var Module Atlas + +type Atlas struct { + plugin.Module + eId string + lastSpoken string +} + +type Message struct { + Result []struct { + Conf float64 + End float64 + Start float64 + Word string + } + Text string +} + +func init() { + config := plugin.Config{ + Name: "atlas", + Type: "module", + Description: "General AI", + Version: "0.0.1", + Author: "Braden Nicholson", + } + Module.Config = config +} + +func (w *Atlas) Setup() (plugin.Config, error) { + err := w.UpdateInterval(2000) + if err != nil { + return plugin.Config{}, err + } + return w.Config, nil +} + +func (w *Atlas) pull() error { + time.Sleep(250 * time.Millisecond) + return nil +} + +func (w *Atlas) Update() error { + if w.Ready() { + err := w.pull() + if err != nil { + return err + } + } + return nil +} + +func (w *Atlas) speak(text string) error { + + timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*15) + // Cancel the timeout of it exits before the timeout is up + defer cancelFunc() + // Prepare the command arguments + args := []string{"-t", text, "-voice", "./pkg/mimic/mycroft_voice_4.0.flitevox"} + // Initialize the command structure + cmd := exec.CommandContext(timeout, "./pkg/mimic/mimic", args...) + // Run and get the stdout and stderr from the output + err := cmd.Run() + if err != nil { + return nil + } + + return nil +} + +func (w *Atlas) retort(text string) error { + + return nil +} + +func (w *Atlas) register() error { + entity := domain.Entity{ + Module: "atlas", + Name: "atlas", + Type: "media", + } + + err := w.Entities.Register(&entity) + if err != nil { + return err + } + + listenBuffer := domain.Attribute{ + Type: "buffer", + Key: "buffer", + Value: "", + Request: "", + Order: 0, + Entity: entity.Id, + Channel: make(chan domain.Attribute), + } + + w.eId = entity.Id + + go func() { + for attribute := range listenBuffer.Channel { + fmt.Println("Atlas hears: " + attribute.Value) + } + }() + + err = w.Attributes.Register(&listenBuffer) + if err != nil { + return err + } + return nil +} + +func (w *Atlas) Run() error { + getwd, err := os.Getwd() + if err != nil { + return err + } + fmt.Println(getwd) + err = w.register() + if err != nil { + return err + } + + u := url.URL{Scheme: "ws", Host: "localhost" + ":" + "2700", Path: ""} + + // Opening websocket connection + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + return err + } + defer c.Close() + + for { + msg := Message{} + err = c.ReadJSON(&msg) + if err != nil { + return err + } + err = w.Attributes.Set(w.eId, "buffer", msg.Text) + if err != nil { + return err + } + + if strings.Contains(msg.Text, "atlas") { + if msg.Text == w.lastSpoken { + continue + } + out := strings.Replace(msg.Text, "atlas", "", 1) + err = w.speak(out) + if err != nil { + return err + } + } + + log.Event("ATLAS: %s", msg.Text) + } + + return nil +} diff --git a/modules/homekit/homekit.go b/modules/homekit/homekit.go index b8c614a..851afeb 100644 --- a/modules/homekit/homekit.go +++ b/modules/homekit/homekit.go @@ -113,7 +113,7 @@ func (h *Homekit) Run() error { } hc.OnTermination(func() { - log.Event("Module 'hs110' is terminating.") + log.Event("Module 'homekit' is terminating.") <-t.Stop() os.Exit(0) }) diff --git a/modules/squid/squid.go b/modules/squid/squid.go index 5d2d718..5806f02 100644 --- a/modules/squid/squid.go +++ b/modules/squid/squid.go @@ -329,7 +329,11 @@ func (s *Squid) pull() error { // Update is called every cycle func (s *Squid) Update() error { if s.Ready() { - err := s.pull() + err := s.UpdateInterval(2000) + if err != nil { + return err + } + err = s.pull() if err != nil { return err } diff --git a/modules/vyos/vyos.go b/modules/vyos/vyos.go index 2bb1766..c0484af 100644 --- a/modules/vyos/vyos.go +++ b/modules/vyos/vyos.go @@ -183,7 +183,7 @@ func (v *Vyos) fetchNetworks() error { defer wg.Done() err = v.scanSubnet(network) if err != nil { - log.Err(err) + return } }() diff --git a/modules/weather/weather.go b/modules/weather/weather.go index f361882..b52001d 100644 --- a/modules/weather/weather.go +++ b/modules/weather/weather.go @@ -162,7 +162,11 @@ func (v *Weather) pull() error { } func (v *Weather) Update() error { if v.Ready() { - err := v.pull() + err := v.UpdateInterval(15000) + if err != nil { + return err + } + err = v.pull() if err != nil { return err } @@ -189,7 +193,8 @@ func (v *Weather) Run() error { if err != nil { return err } - forecast := domain.Attribute{ + + forecast := &domain.Attribute{ Key: "forecast", Value: buffer, Request: buffer, @@ -199,7 +204,7 @@ func (v *Weather) Run() error { } v.eId = e.Id - err = v.Attributes.Register(&forecast) + err = v.Attributes.Register(forecast) if err != nil { return err } diff --git a/modules/webstats/webstats.go b/modules/webstats/webstats.go index a932bd3..66a064c 100644 --- a/modules/webstats/webstats.go +++ b/modules/webstats/webstats.go @@ -4,7 +4,6 @@ package main import ( "time" - "udap/internal/log" "udap/pkg/plugin" ) @@ -50,8 +49,5 @@ func (w *WebStats) Update() error { } func (w *WebStats) Run() error { - log.Log("Webstats running") - time.Sleep(time.Second * 2) - log.Log("Webstats exiting") return nil } diff --git a/pkg/plugin/default.go b/pkg/plugin/default.go index 1ed718c..87f9ed1 100644 --- a/pkg/plugin/default.go +++ b/pkg/plugin/default.go @@ -31,7 +31,7 @@ func (m *Module) UpdateInterval(frequency int) error { // Ready is called once at the launch of the module func (m *Module) Ready() bool { - return time.Since(m.LastUpdate) > time.Duration(m.Frequency)*time.Millisecond + return time.Since(m.LastUpdate).Milliseconds() >= (time.Duration(m.Frequency) * time.Millisecond).Milliseconds() } // Connect is called once at the launch of the module From 51dbf3b3efbebc3175617de5d82881610eb3aa45 Mon Sep 17 00:00:00 2001 From: Braden Date: Thu, 2 Jun 2022 17:11:42 -0700 Subject: [PATCH 06/18] Fixed reactive emission for endpoints on enrollment. Added more support for Atlas. Made several tweaks to the backend in order to ensure compatability with nexus. Reimplemented remote module lifecycle management. Reintroduced pulse timings for all subsystems. Updated README.md. --- .gitignore | 1 + README.md | 14 ++- internal/controller/controller.go | 12 +-- internal/core/modules/attribute/operator.go | 33 ++++--- internal/core/modules/attribute/repository.go | 22 +---- internal/core/modules/attribute/service.go | 14 ++- internal/core/modules/endpoint/operator.go | 3 + internal/core/modules/module/operator.go | 26 ++--- internal/core/modules/module/repository.go | 6 +- internal/core/modules/module/service.go | 99 ++++++++++++++++--- internal/orchestrator/orchestrator.go | 79 ++++++++------- internal/port/routes/attribute.go | 49 +++++++++ internal/port/routes/module.go | 40 ++++---- internal/pulse/pulse.go | 10 +- modules/atlas/atlas.go | 49 +++++---- modules/govee/govee.go | 25 +++-- modules/weather/weather.go | 22 ++--- modules/webstats/webstats.go | 8 +- pkg/plugin/default.go | 8 +- 19 files changed, 347 insertions(+), 173 deletions(-) create mode 100644 internal/port/routes/attribute.go diff --git a/.gitignore b/.gitignore index dde6562..27a617f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ spotify.json # Client Ignore +/client/node_modules/** /client/src/assets/js/ /client/src/assets/**.css /client/src/assets/**.css.map diff --git a/README.md b/README.md index af82c46..216f484 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# UDAP v2.10.1 +# UDAP v2.13 beta-2 + [![Go](https://github.com/bradenn/udap/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/bradenn/udap/actions/workflows/go.yml) [![Typescript](https://github.com/bradenn/udap/actions/workflows/ts.yml/badge.svg)](https://github.com/bradenn/udap/actions/workflows/ts.yml) + ## Universal Device Aggregation Platform Udap aims to efficiently link and aggregate many unlike interfaces into a heuristic model that can be manipulated and @@ -30,4 +32,14 @@ Both modules and Endpoints are permitted to make modification to entities and at can concurrently modify multiple entities and attributes at a time. The command buffer can be modified to accept 4096 concurrent commands, but for larger loads, instancing is recommended. +## Glossary + +| Command | Description | +|----------| --- | +| U.D.A.P. | Universal Device Aggregation Platform (encompasses all below terms) | +| Core | The physical running UDAP program instance | +| Nexus | The front-end interface for all of UDAP | +| Terminal | An authoritative nexus instance (Used for configuration and management) | +| Pad | A general use nexus instance, can be used by anyone without authentication if configured by terminal. | + #### Copyright © 2019-2022 Braden Nicholson diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 907a277..aa66c6c 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -40,22 +40,22 @@ func NewController(db *gorm.DB) (*Controller, error) { func (c *Controller) Listen(resp chan domain.Mutation) { - err := c.Modules.Watch(resp) + err := c.Attributes.Watch(resp) if err != nil { return } - err = c.Endpoints.Watch(resp) + err = c.Entities.Watch(resp) if err != nil { return } - err = c.Entities.Watch(resp) + err = c.Modules.Watch(resp) if err != nil { return } - err = c.Attributes.Watch(resp) + err = c.Endpoints.Watch(resp) if err != nil { return } @@ -64,12 +64,12 @@ func (c *Controller) Listen(resp chan domain.Mutation) { func (c *Controller) EmitAll() error { - err := c.Entities.EmitAll() + err := c.Attributes.EmitAll() if err != nil { return err } - err = c.Attributes.EmitAll() + err = c.Entities.EmitAll() if err != nil { return err } diff --git a/internal/core/modules/attribute/operator.go b/internal/core/modules/attribute/operator.go index 5c12a17..960f771 100644 --- a/internal/core/modules/attribute/operator.go +++ b/internal/core/modules/attribute/operator.go @@ -4,17 +4,20 @@ package attribute import ( "fmt" + "sync" "time" "udap/internal/core/domain" ) type attributeOperator struct { hooks map[string]chan domain.Attribute + mutex sync.RWMutex } func NewOperator() domain.AttributeOperator { return &attributeOperator{ hooks: map[string]chan domain.Attribute{}, + mutex: sync.RWMutex{}, } } @@ -22,34 +25,44 @@ func (a *attributeOperator) Register(attribute *domain.Attribute) error { if attribute.Id == "" { return fmt.Errorf("invalid attribute id") } + a.mutex.Lock() a.hooks[attribute.Id] = attribute.Channel + a.mutex.Unlock() return nil } func (a *attributeOperator) Request(attribute *domain.Attribute, s string) error { + var channel chan domain.Attribute + + a.mutex.Lock() + channel = a.hooks[attribute.Id] + a.mutex.Unlock() + + if channel == nil { + return fmt.Errorf("channel is not set") + } + + attribute.Request = s + + channel <- *attribute + + attribute.Requested = time.Now() + err := a.Set(attribute, s) if err != nil { return err } + return nil } func (a *attributeOperator) Set(attribute *domain.Attribute, s string) error { // If the attribute handler is not set, return an error - channel := a.hooks[attribute.Id] attribute.Request = s attribute.Value = s - attribute.Requested = time.Now() - - if channel == nil { - return nil - } - - channel <- *attribute - return nil } @@ -58,8 +71,6 @@ func (a *attributeOperator) Update(attribute *domain.Attribute, val string, stam if attribute.Requested.Before(stamp) && attribute.Request != val && time.Since(attribute.Requested) < 5*time.Second { return fmt.Errorf("OVERWRITES REQUEST") } - // Update the request value (since the request can be external) - attribute.Request = val // Set the value err := a.Set(attribute, val) if err != nil { diff --git a/internal/core/modules/attribute/repository.go b/internal/core/modules/attribute/repository.go index 932de36..dd3a750 100644 --- a/internal/core/modules/attribute/repository.go +++ b/internal/core/modules/attribute/repository.go @@ -18,19 +18,7 @@ func NewRepository(db *gorm.DB) domain.AttributeRepository { } func (u *attributeRepo) Register(attribute *domain.Attribute) error { - if attribute.Id == "" { - err := u.db.Model(&domain.Attribute{}).Where("entity = ? AND key = ?", - attribute.Entity, attribute.Key).FirstOrCreate(attribute).Error - if err != nil { - return err - } - } else { - err := u.db.Model(&domain.Attribute{}).Where("id = ?", attribute.Id).First(attribute).Error - if err != nil { - return err - } - } - err := u.db.Model(&domain.Attribute{}).Where("id = ?", attribute.Id).Save(attribute).Error + err := u.db.Model(&domain.Attribute{}).Where(attribute).FirstOrCreate(attribute).Error if err != nil { return err } @@ -47,16 +35,16 @@ func (u *attributeRepo) FindByComposite(entity string, key string) (*domain.Attr } func (u *attributeRepo) FindAllByEntity(entity string) (*[]domain.Attribute, error) { - var target *[]domain.Attribute - if err := u.db.Where("entity = ?", entity).Find(target).Error; err != nil { + var target []domain.Attribute + if err := u.db.Where("entity = ?", entity).Find(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (u *attributeRepo) FindAll() (*[]domain.Attribute, error) { var target []domain.Attribute - if err := u.db.First(&target).Error; err != nil { + if err := u.db.Find(&target).Error; err != nil { return nil, err } return &target, nil diff --git a/internal/core/modules/attribute/service.go b/internal/core/modules/attribute/service.go index 95b9b06..189b339 100644 --- a/internal/core/modules/attribute/service.go +++ b/internal/core/modules/attribute/service.go @@ -70,6 +70,7 @@ func (u *attributeService) Register(attribute *domain.Attribute) error { if err != nil { return err } + err = u.emit(attribute) if err != nil { return err @@ -86,6 +87,10 @@ func (u *attributeService) Request(entity string, key string, value string) erro if err != nil { return err } + err = u.repository.Update(e) + if err != nil { + return err + } err = u.emit(e) if err != nil { return err @@ -98,7 +103,9 @@ func (u *attributeService) Set(entity string, key string, value string) error { if err != nil { return err } - err = u.operator.Set(e, value) + e.Request = value + e.Value = value + err = u.repository.Update(e) if err != nil { return err } @@ -118,7 +125,10 @@ func (u *attributeService) Update(entity string, key string, value string, stamp if err != nil { return err } - + err = u.repository.Update(e) + if err != nil { + return err + } err = u.emit(e) if err != nil { return err diff --git a/internal/core/modules/endpoint/operator.go b/internal/core/modules/endpoint/operator.go index ee7ca93..f91045b 100644 --- a/internal/core/modules/endpoint/operator.go +++ b/internal/core/modules/endpoint/operator.go @@ -160,14 +160,17 @@ func (m *endpointOperator) Enroll(endpoint *domain.Endpoint, conn *websocket.Con log.Event("Endpoint '%s' connected.", endpoint.Name) wg := sync.WaitGroup{} wg.Add(1) + go func() { defer wg.Done() connection.Watch() }() + err = m.controller.EmitAll() if err != nil { return err } + wg.Wait() return nil } diff --git a/internal/core/modules/module/operator.go b/internal/core/modules/module/operator.go index 05a91b1..632a66d 100644 --- a/internal/core/modules/module/operator.go +++ b/internal/core/modules/module/operator.go @@ -30,22 +30,26 @@ func NewOperator(ctrl *controller.Controller) domain.ModuleOperator { } func (m *moduleOperator) Update(module *domain.Module) error { - if m.modules[module.Name] == nil { - return fmt.Errorf("nothing to update") - } - pulse.Begin(module.Id) - err := m.modules[module.Name].Update() - pulse.End(module.Id) - if err != nil { - return err + if module.Enabled { + if m.modules[module.Name] == nil { + return fmt.Errorf("nothing to update") + } + pulse.Begin(module.Id) + err := m.modules[module.Name].Update() + pulse.End(module.Id) + if err != nil { + return err + } } return nil } func (m *moduleOperator) Run(module *domain.Module) error { - err := m.modules[module.Name].Run() - if err != nil { - return err + if module.Enabled { + err := m.modules[module.Name].Run() + if err != nil { + return err + } } return nil } diff --git a/internal/core/modules/module/repository.go b/internal/core/modules/module/repository.go index 37bd2a4..47ed368 100644 --- a/internal/core/modules/module/repository.go +++ b/internal/core/modules/module/repository.go @@ -34,11 +34,11 @@ func (m moduleRepo) FindAll() (*[]domain.Module, error) { } func (m moduleRepo) FindById(id string) (*domain.Module, error) { - var target *domain.Module - if err := m.db.Where("id = ?", id).First(target).Error; err != nil { + var target domain.Module + if err := m.db.Where("id = ?", id).First(&target).Error; err != nil { return nil, err } - return target, nil + return &target, nil } func (m moduleRepo) Create(module *domain.Module) error { diff --git a/internal/core/modules/module/service.go b/internal/core/modules/module/service.go index da4b5f6..b0895a6 100644 --- a/internal/core/modules/module/service.go +++ b/internal/core/modules/module/service.go @@ -84,16 +84,14 @@ func (u *moduleService) UpdateAll() error { return err } wg := sync.WaitGroup{} - wg.Add(len(*modules)) - for _, module := range *modules { + ref := *modules + wg.Add(len(ref)) + for _, module := range ref { go func(mod domain.Module) { defer wg.Done() - if mod.Enabled { - err = u.Update(&mod) - if err != nil { - log.Err(err) - return - } + err = u.Update(&mod) + if err != nil { + log.Err(err) } }(module) } @@ -101,6 +99,24 @@ func (u *moduleService) UpdateAll() error { return nil } +const ( + DISCOVERED = "discovered" + UNINITIALIZED = "uninitialized" + IDLE = "idle" + RUNNING = "running" + STOPPED = "stopped" + ERROR = "error" +) + +func (u *moduleService) setState(module *domain.Module, state string) error { + module.State = state + err := u.repository.Update(module) + if err != nil { + return err + } + return nil +} + func (u *moduleService) RunAll() error { modules, err := u.repository.FindAll() if err != nil { @@ -108,6 +124,10 @@ func (u *moduleService) RunAll() error { } for _, module := range *modules { + err = u.setState(&module, RUNNING) + if err != nil { + return err + } go func(mod domain.Module) { err = u.Run(&mod) if err != nil { @@ -132,6 +152,11 @@ func (u *moduleService) LoadAll() error { err = u.Load(&mod) if err != nil { log.Err(err) + return + } + err = u.setState(&mod, IDLE) + if err != nil { + return } }(module) } @@ -163,6 +188,7 @@ func (u moduleService) Discover() error { target = &domain.Module{} target.Name = name target.Path = p + target.State = DISCOVERED err = u.repository.Create(target) if err != nil { return err @@ -172,7 +198,7 @@ func (u moduleService) Discover() error { return nil } -func (u moduleService) BuildAll() error { +func (u *moduleService) BuildAll() error { modules, err := u.repository.FindAll() if err != nil { return err @@ -185,6 +211,15 @@ func (u moduleService) BuildAll() error { err = u.Build(&mod) if err != nil { log.Err(err) + err = u.setState(&mod, ERROR) + if err != nil { + return + } + return + } + err = u.setState(&mod, UNINITIALIZED) + if err != nil { + return } }(module) } @@ -202,22 +237,54 @@ func (u moduleService) FindByName(name string) (*domain.Module, error) { return u.repository.FindByName(name) } -func (u moduleService) Disable(name string) error { - _, err := u.FindByName(name) +func (u moduleService) Disable(id string) error { + module, err := u.repository.FindById(id) + if err != nil { + return err + } + module.Enabled = false + err = u.repository.Update(module) + if err != nil { + return err + } + err = u.emit(module) if err != nil { return err } return nil } -func (u moduleService) Enable(name string) error { - // TODO implement me - panic("implement me") +func (u moduleService) save(module *domain.Module) error { + err := u.repository.Update(module) + if err != nil { + return err + } + err = u.emit(module) + if err != nil { + return err + } + return nil +} + +func (u moduleService) Enable(id string) error { + module, err := u.repository.FindById(id) + if err != nil { + return err + } + module.Enabled = true + err = u.repository.Update(module) + if err != nil { + return err + } + err = u.emit(module) + if err != nil { + return err + } + return nil } func (u moduleService) Reload(name string) error { - // TODO implement me - panic("implement me") + return nil } func (u moduleService) Halt(name string) error { diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index bf5f4e7..062ccbe 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -6,6 +6,7 @@ import ( "github.com/go-chi/chi" "gorm.io/gorm" "net/http" + "sync" "time" "udap/internal/controller" "udap/internal/core" @@ -83,22 +84,25 @@ func (o *orchestrator) Update() error { func (o *orchestrator) Run() error { - resp := make(chan domain.Mutation, 8) + resp := make(chan domain.Mutation, 20) o.controller.Listen(resp) + wg := sync.WaitGroup{} + wg.Add(3) go func() { + defer wg.Done() for response := range resp { err := o.endpoints.SendAll(response.Id, response.Operation, response.Body) if err != nil { log.Err(err) - return + continue } } - }() // Initialize and route applicable domains routes.NewUserRouter(o.controller.Users).RouteUsers(o.router) + routes.NewAttributeRouter(o.controller.Attributes).RouteAttributes(o.router) routes.NewEndpointRouter(o.endpoints).RouteEndpoints(o.router) routes.NewModuleRouter(o.modules).RouteModules(o.router) @@ -107,44 +111,53 @@ func (o *orchestrator) Run() error { o.server = &http.Server{Addr: ":3020", Handler: o.router} go func() { + defer wg.Done() err := o.server.ListenAndServe() if err != nil { log.ErrF(err, "http server exited with error:\n") } + log.Event("Server exited") }() - delay := 1000.0 - for { - pulse.Begin("update") - select { - case <-time.After(time.Millisecond * time.Duration(delay)): - log.Event("Orchestrator event loop timed out") - continue - default: - start := time.Now() - - err := o.Update() - if err != nil { - log.ErrF(err, "runtime update error: %s") - } - - delta := time.Since(start) - duration := (time.Millisecond * time.Duration(delay)) - delta - if duration > 0 { + go func() { + defer wg.Done() + delay := time.Millisecond * 1000 + for { + pulse.Begin("update") + select { + case <-time.After(delay): + log.Event("Orchestrator event loop timed out") + continue + default: + start := time.Now() + + err := o.Update() + if err != nil { + log.ErrF(err, "runtime update error: %s") + } + + delta := time.Since(start) + duration := delay - delta log.Event("Tick Complete (%s)", delta) - time.Sleep(duration) + if duration > 0 && duration < delay { + time.Sleep(duration) + } + break } - - } - pulse.End("update") - timings := pulse.Timings.Timings() - for s, proc := range timings { - resp <- domain.Mutation{ - Status: "update", - Operation: "timing", - Body: proc, - Id: s, + pulse.End("update") + timings := pulse.Timings.Timings() + for s, proc := range timings { + resp <- domain.Mutation{ + Status: "update", + Operation: "timing", + Body: proc, + Id: s, + } } } - } + log.Event("Event loops exited") + }() + + wg.Wait() + return nil } diff --git a/internal/port/routes/attribute.go b/internal/port/routes/attribute.go new file mode 100644 index 0000000..3f97925 --- /dev/null +++ b/internal/port/routes/attribute.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Braden Nicholson + +package routes + +import ( + "bytes" + "github.com/go-chi/chi" + "net/http" + "udap/internal/core/domain" + "udap/internal/log" +) + +type AttributeRouter interface { + RouteAttributes(router chi.Router) +} + +type attributeRouter struct { + service domain.AttributeService +} + +func NewAttributeRouter(service domain.AttributeService) AttributeRouter { + return &attributeRouter{ + service: service, + } +} + +func (r *attributeRouter) RouteAttributes(router chi.Router) { + router.Route("/entities/{id}/attributes/{key}", func(local chi.Router) { + local.Post("/request", r.request) + }) +} + +func (r *attributeRouter) request(w http.ResponseWriter, req *http.Request) { + id := chi.URLParam(req, "id") + key := chi.URLParam(req, "key") + buf := bytes.Buffer{} + _, err := buf.ReadFrom(req.Body) + if err != nil { + w.WriteHeader(400) + } + log.Event("Request '%s' = %s", key, buf.String()) + if id != "" && key != "" { + err := r.service.Request(id, key, buf.String()) + if err != nil { + w.WriteHeader(400) + } + } + w.WriteHeader(200) +} diff --git a/internal/port/routes/module.go b/internal/port/routes/module.go index 38af2f8..7ab2f8a 100644 --- a/internal/port/routes/module.go +++ b/internal/port/routes/module.go @@ -17,26 +17,24 @@ type moduleRouter struct { } func NewModuleRouter(service domain.ModuleService) ModuleRouter { - return moduleRouter{ + return &moduleRouter{ service: service, } } -func (r moduleRouter) RouteModules(router chi.Router) { - router.Route("/modules", func(local chi.Router) { - local.Route("/{name}", func(named chi.Router) { - named.Post("/build", r.build) - named.Post("/disable", r.disable) - named.Post("/enable", r.enable) - named.Post("/halt", r.halt) - }) +func (r *moduleRouter) RouteModules(router chi.Router) { + router.Route("/modules/{id}", func(local chi.Router) { + local.Post("/build", r.build) + local.Post("/disable", r.disable) + local.Post("/enable", r.enable) + local.Post("/halt", r.halt) }) } func (r moduleRouter) build(w http.ResponseWriter, req *http.Request) { - name := chi.URLParam(req, "name") - if name != "" { - mod, err := r.service.FindByName(name) + id := chi.URLParam(req, "id") + if id != "" { + mod, err := r.service.FindByName(id) if err != nil { return } @@ -49,9 +47,9 @@ func (r moduleRouter) build(w http.ResponseWriter, req *http.Request) { } func (r moduleRouter) enable(w http.ResponseWriter, req *http.Request) { - name := chi.URLParam(req, "name") - if name != "" { - err := r.service.Enable(name) + id := chi.URLParam(req, "id") + if id != "" { + err := r.service.Enable(id) if err != nil { http.Error(w, "invalid module name", 401) } @@ -60,9 +58,9 @@ func (r moduleRouter) enable(w http.ResponseWriter, req *http.Request) { } func (r moduleRouter) disable(w http.ResponseWriter, req *http.Request) { - name := chi.URLParam(req, "name") - if name != "" { - err := r.service.Disable(name) + id := chi.URLParam(req, "id") + if id != "" { + err := r.service.Disable(id) if err != nil { http.Error(w, "invalid module name", 401) } @@ -71,9 +69,9 @@ func (r moduleRouter) disable(w http.ResponseWriter, req *http.Request) { } func (r moduleRouter) halt(w http.ResponseWriter, req *http.Request) { - name := chi.URLParam(req, "name") - if name != "" { - err := r.service.Halt(name) + id := chi.URLParam(req, "id") + if id != "" { + err := r.service.Halt(id) if err != nil { http.Error(w, "invalid module name", 401) } diff --git a/internal/pulse/pulse.go b/internal/pulse/pulse.go index 3aad627..d9c6d4f 100644 --- a/internal/pulse/pulse.go +++ b/internal/pulse/pulse.go @@ -12,17 +12,17 @@ var Timings Timing func init() { Timings = Timing{} - Timings.mt = sync.Mutex{} + Timings.mt = sync.RWMutex{} Timings.history = map[string]Proc{} Timings.waiting = map[string]Proc{} - Timings.handler = make(chan Proc) + Timings.handler = make(chan Proc, 4) go Timings.handle() } type Timing struct { waiting map[string]Proc handler chan Proc - mt sync.Mutex + mt sync.RWMutex history map[string]Proc } @@ -39,11 +39,11 @@ type Proc struct { func (h *Timing) Timings() (a map[string]Proc) { a = map[string]Proc{} - h.mt.Lock() + h.mt.RLock() for i, u := range h.history { a[i] = u } - h.mt.Unlock() + h.mt.RUnlock() return } diff --git a/modules/atlas/atlas.go b/modules/atlas/atlas.go index ce8e6d7..26f2cdf 100644 --- a/modules/atlas/atlas.go +++ b/modules/atlas/atlas.go @@ -6,8 +6,8 @@ import ( "context" "fmt" "github.com/gorilla/websocket" + "github.com/kevwan/chatbot/bot" "net/url" - "os" "os/exec" "strings" "time" @@ -22,6 +22,7 @@ type Atlas struct { plugin.Module eId string lastSpoken string + chatbot *bot.ChatBot } type Message struct { @@ -54,16 +55,14 @@ func (w *Atlas) Setup() (plugin.Config, error) { } func (w *Atlas) pull() error { - time.Sleep(250 * time.Millisecond) + return nil } func (w *Atlas) Update() error { - if w.Ready() { - err := w.pull() - if err != nil { - return err - } + if time.Since(w.Module.LastUpdate) >= time.Second*2 { + w.Module.LastUpdate = time.Now() + return w.pull() } return nil } @@ -88,6 +87,25 @@ func (w *Atlas) speak(text string) error { func (w *Atlas) retort(text string) error { + // responses := map[string]string{} + // + // if text == "" { + // + // } + // responses["what is the meaning of life"] = "the definitive answer to the meaning of life is forty two." + // responses["fuck you"] = "I'd rather not" + // responses["fuck yourself"] = "Since I do not physically exist, that would be quite difficult." + // + // for s := range responses { + // if s == text { + // err := w.speak(responses[s]) + // if err != nil { + // return err + // } + // return nil + // } + // } + return nil } @@ -129,12 +147,8 @@ func (w *Atlas) register() error { } func (w *Atlas) Run() error { - getwd, err := os.Getwd() - if err != nil { - return err - } - fmt.Println(getwd) - err = w.register() + + err := w.register() if err != nil { return err } @@ -160,11 +174,11 @@ func (w *Atlas) Run() error { } if strings.Contains(msg.Text, "atlas") { - if msg.Text == w.lastSpoken { - continue + if strings.HasPrefix(msg.Text, "the") { + msg.Text = strings.Replace(msg.Text, "the ", "", 1) } - out := strings.Replace(msg.Text, "atlas", "", 1) - err = w.speak(out) + msg.Text = strings.Replace(msg.Text, "atlas ", "", 1) + err = w.retort(msg.Text) if err != nil { return err } @@ -173,5 +187,4 @@ func (w *Atlas) Run() error { log.Event("ATLAS: %s", msg.Text) } - return nil } diff --git a/modules/govee/govee.go b/modules/govee/govee.go index 5d68b15..98d61bb 100644 --- a/modules/govee/govee.go +++ b/modules/govee/govee.go @@ -491,7 +491,13 @@ func (g *Govee) push() error { } }(d, s) } - wg.Wait() + select { + case <-time.After(time.Millisecond * 1000): + return nil + default: + wg.Wait() + } + return nil } @@ -510,7 +516,6 @@ func (g *Govee) Run() error { } for _, device := range devices { - s := &domain.Entity{ Name: device.DeviceName, Type: "spectrum", @@ -523,14 +528,16 @@ func (g *Govee) Run() error { g.devices[s.Id] = device attributes := GenerateAttributes(s.Id) for _, attribute := range attributes { - go func() { - for attr := range attribute.Channel { - err = g.statePut(device, attribute.Key, s.Id)(attr.Request) + go func(dev Device, channel chan domain.Attribute) { + for attr := range channel { + log.Event("Request %s.%s=%s", dev.DeviceName, attr.Key, attr.Request) + err = g.statePut(dev, attr.Key, s.Id)(attr.Request) if err != nil { - return + log.Err(err) + continue } } - }() + }(device, attribute.Channel) err = g.Attributes.Register(attribute) if err != nil { return err @@ -538,5 +545,9 @@ func (g *Govee) Run() error { } } + err = g.push() + if err != nil { + return err + } return nil } diff --git a/modules/weather/weather.go b/modules/weather/weather.go index b52001d..ecea76e 100644 --- a/modules/weather/weather.go +++ b/modules/weather/weather.go @@ -6,7 +6,6 @@ import ( "bytes" "encoding/json" "net/http" - "time" "udap/internal/core/domain" "udap/pkg/plugin" ) @@ -154,7 +153,7 @@ func (v *Weather) pull() error { if err != nil { return err } - err = v.Attributes.Update(v.eId, "forecast", buffer, time.Now()) + err = v.Attributes.Set(v.eId, "forecast", buffer) if err != nil { return err } @@ -175,29 +174,21 @@ func (v *Weather) Update() error { } func (v *Weather) Run() error { - err := v.fetchWeather() - if err != nil { - return err - } e := &domain.Entity{ Name: "weather", Module: "weather", Type: "media", } - err = v.Entities.Register(e) - if err != nil { - return err - } - buffer, err := v.forecastBuffer() + err := v.Entities.Register(e) if err != nil { return err } forecast := &domain.Attribute{ Key: "forecast", - Value: buffer, - Request: buffer, + Value: "{}", + Request: "{}", Type: "media", Order: 0, Entity: e.Id, @@ -209,5 +200,10 @@ func (v *Weather) Run() error { return err } + err = v.pull() + if err != nil { + return err + } + return nil } diff --git a/modules/webstats/webstats.go b/modules/webstats/webstats.go index 66a064c..aa9536a 100644 --- a/modules/webstats/webstats.go +++ b/modules/webstats/webstats.go @@ -39,11 +39,9 @@ func (w *WebStats) pull() error { } func (w *WebStats) Update() error { - if w.Ready() { - err := w.pull() - if err != nil { - return err - } + if time.Since(w.Module.LastUpdate) >= time.Second*2 { + w.Module.LastUpdate = time.Now() + return w.pull() } return nil } diff --git a/pkg/plugin/default.go b/pkg/plugin/default.go index 87f9ed1..2e5d0a9 100644 --- a/pkg/plugin/default.go +++ b/pkg/plugin/default.go @@ -18,20 +18,20 @@ type Config struct { type Module struct { Config LastUpdate time.Time - Frequency int + Frequency time.Duration *controller.Controller } // UpdateInterval is called once at the launch of the module -func (m *Module) UpdateInterval(frequency int) error { +func (m *Module) UpdateInterval(frequency time.Duration) error { m.LastUpdate = time.Now() - m.Frequency = frequency + m.Frequency = time.Millisecond * frequency return nil } // Ready is called once at the launch of the module func (m *Module) Ready() bool { - return time.Since(m.LastUpdate).Milliseconds() >= (time.Duration(m.Frequency) * time.Millisecond).Milliseconds() + return time.Since(m.LastUpdate) >= m.Frequency } // Connect is called once at the launch of the module From 7a5a8511bcad830ee4bbe9d84c0cb2d2769cc199 Mon Sep 17 00:00:00 2001 From: Braden Date: Thu, 2 Jun 2022 17:16:35 -0700 Subject: [PATCH 07/18] Created an Atlas app on nexus. Improved compatability with the upcoming UDAP v2.13 release. Fixed bugs. Improved websocket keep-alive and state management. Fixed a bug where the connection status didn't update on disconnect. Added multiple voice selections to the atlas app. Various UI tweaks and improvements. --- client/package.json | 6 +- client/src/App.vue | 9 +- client/src/assets/sass/app.css.map | 2 +- client/src/assets/sass/app.scss | 2 +- client/src/assets/sass/element.scss | 65 +- client/src/components/AllocationBar.vue | 88 +- client/src/components/IdTag.vue | 24 +- client/src/components/entity/Attribute.vue | 2 +- client/src/components/plot/Plot.vue | 10 +- client/src/components/plot/Radio.vue | 3 - client/src/components/widgets/Light.vue | 19 +- client/src/components/widgets/Macros.vue | 4 +- client/src/components/widgets/Spotify.vue | 34 +- client/src/components/widgets/Weather.vue | 4 +- client/src/components/zone/ZonePreview.vue | 4 +- client/src/router/index.ts | 32 +- client/src/types.ts | 1 + client/src/views/terminal/Energy.vue | 270 +- client/src/views/terminal/Home.vue | 30 +- client/src/views/terminal/Terminal.vue | 13 +- client/src/views/terminal/atlas/Atlas.vue | 14 + .../views/terminal/atlas/AtlasOverview.vue | 56 + .../views/terminal/atlas/AtlasSettings.vue | 115 + client/src/views/terminal/nexus.ts | 27 +- .../views/terminal/settings/Connection.vue | 14 +- .../src/views/terminal/settings/Settings.vue | 2 +- .../src/views/terminal/settings/Timings.vue | 3 +- .../terminal/settings/module/Modules.vue | 16 +- .../terminal/settings/zone/CreateZone.vue | 11 +- .../views/terminal/settings/zone/Zones.vue | 2 +- client/yarn.lock | 3922 ----------------- eeee.uml | 1030 +++++ go.mod | 12 +- 33 files changed, 1653 insertions(+), 4193 deletions(-) create mode 100644 client/src/views/terminal/atlas/Atlas.vue create mode 100644 client/src/views/terminal/atlas/AtlasOverview.vue create mode 100644 client/src/views/terminal/atlas/AtlasSettings.vue delete mode 100644 client/yarn.lock create mode 100644 eeee.uml diff --git a/client/package.json b/client/package.json index 9209583..2452213 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "udap-nexus", "version": "2.10.9", - "main": "electron/main.js", + "main": "/src/main.js", "scripts": { "dev": "vue-tsc --noEmit && vite", "build": "vue-tsc --noEmit && vite build", @@ -12,12 +12,9 @@ "@types/websocket": "^1.0.5", "axios": "^0.21.3", "bootstrap": "^5.1.0", - "colorthief": "^2.3.2", - "electron": "^16.0.5", "glob": "^8.0.1", "minimist": "~1.2.6", "moment": "^2.29.2", - "npm": "^8.5.3", "qrcanvas-vue": "^3.0.0", "qrcode": "^1.5.0", "sass": "^1.45.1", @@ -36,6 +33,7 @@ "@vitejs/plugin-vue": "^1.6.0", "@vue/compiler-sfc": "^3.2.6", "@vue/tsconfig": "^0.1.3", + "esbuild": "^0.14.42", "typescript": "~4.5.5", "vite": "^2.5.2", "vue-tsc": "^0.31.4" diff --git a/client/src/App.vue b/client/src/App.vue index 320b51f..dbe8ab0 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -6,6 +6,7 @@ import {version} from "../package.json"; interface Preferences { ui: { + blurBg: boolean background: string theme: string mode: string @@ -20,6 +21,7 @@ interface Preferences { const preferenceDefaults: Preferences = { ui: { + blurBg: false, blur: 6, background: "milk", mode: "cursor", @@ -123,7 +125,8 @@ provide('hideHome', hideHome)
- Background + Background
@@ -171,6 +174,10 @@ provide('hideHome', hideHome) animation: switch 0.25s ease-in-out forwards; } +.backdrop-blurred { + filter: blur(4px); +} + .backdrop:after { } diff --git a/client/src/assets/sass/app.css.map b/client/src/assets/sass/app.css.map index 7deae83..c18f732 100644 --- a/client/src/assets/sass/app.css.map +++ b/client/src/assets/sass/app.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["element.scss","app.scss"],"names":[],"mappings":"AAmBA;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAMF;EACE;IACE;;EAEF;IACE;;;AAyBJ;AAUI;EACE;;AAIF;EAEE;;;AAPF;EACE;;AAIF;EAEE;;;AAWN;EACE,kBAxFW;EAyFX;;;AAWF;EAEE;EACA;EACA;EACA;;;AAKF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKF;EACE;EACA;;;AAIF;EACE;EACA;EACA;;;AAGF;EACE;;;AAIF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;;AClJJ;EACE;EACA;EACA;EACA;;;AAIF;EACE;;;AAIF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAiCF;AAEE;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AAKJ;AAiBE;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAqBZ;AASE;EACE,aATU;;;AAQZ;EACE,aATU;;;AAQZ;EACE,aATU;;;AAQZ;EACE,aATU;;;AAad;AAcE;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASJ;EACE;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACE;EACA;;;AAGF;EACE;;;AAOF;EACE;;;AAMF;EAGE;EACA;EAEA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EAEE;EACA;EACA;EACA;;;AAIF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA,kBD/MW;;;ACmNb;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EAEA;EAGA;EAGA;EACA;EACA;;;AAGF;EACE;;;AAIF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAIA;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAKJ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EAGA;EACA;;;AAGF;EAGE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBDlUW;ECmUX;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAWF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAIF;EAEE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAIF;EAEE;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAnaW;EAoaX;EAEA;EACA;EACA;;;AAGF;AACA;EAEE;EACA;;;AAGF;AACA;EACE;EACA;;;AAIF;AACA;EACE;EACA;;;AAGF;AAMA;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA,kBDvfW;ECwfX;EACA;EACA;EACA;EACA;EACA;EAGA;;;AAQF;EAEE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EAEE;;;AAKF;EACE;AAA6B;EAC7B;AAA2B;EAC3B;AAAwB;EACxB;AAAuB;EACvB;AACA;AAAA;;;AAKF;EACE;;;AAGF;EACE,kBAhpBI;;;AAmpBN;AAEA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EAEA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EAEA;;;AAGF;EACE;EAGA;;;AAIF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA;EAEE;EACA;EACA;EAEA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAWF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA","file":"app.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["element.scss","app.scss"],"names":[],"mappings":"AAoBA;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EAGA;;;AAGF;EACE;EACA;EACA;;;AAKF;EACE;IACE;;EAEF;IACE;;;AAOJ;AAUI;EACE;;;AADF;EACE;;;AAYN;EACE,kBA9EW;EA+EX;;;AAWF;EAGE;EAEA;EACA;EACA;;;AAKF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKF;EACE;EACA;;;AAIF;EACE;EACA;EACA;;;AAGF;EACE;;;AAIF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;;ACvIJ;EACE;EACA;EACA;EACA;;;AAIF;EACE;;;AAIF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAiCF;AAEE;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AAKJ;AAiBE;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAgBV;EACE,WAjBQ;;;AAqBZ;AASE;EACE,aATU;;;AAQZ;EACE,aATU;;;AAQZ;EACE,aATU;;;AAQZ;EACE,aATU;;;AAad;AAcE;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AADF;EACE;;;AASJ;EACE;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACE;EACA;;;AAGF;EACE;;;AAOF;EACE;;;AAMF;EAGE;EACA;EAEA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EAEE;EACA;EACA;EACA;;;AAIF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA,kBD/MW;;;ACmNb;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EAEA;EAGA;EAGA;EACA;EACA;;;AAGF;EACE,kBD7OkB;;;ACiPpB;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAIA;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAFF;EACE;EACA;;;AAKJ;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EAGA;EACA;;;AAGF;EAGE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBDlUW;ECmUX;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAWF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAIF;EAEE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAIF;EAEE;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAnaW;EAoaX;EAEA;EACA;EACA;;;AAGF;AACA;EAEE;EACA;;;AAGF;AACA;EACE;EACA;;;AAIF;AACA;EACE;EACA;;;AAGF;AAMA;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA,kBDvfW;ECwfX;EACA;EACA;EACA;EACA;EACA;EAGA;;;AAQF;EAEE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EAEE;;;AAKF;EACE;AAA6B;EAC7B;AAA2B;EAC3B;AAAwB;EACxB;AAAuB;EACvB;AACA;AAAA;;;AAKF;EACE;;;AAGF;EACE,kBAhpBI;;;AAmpBN;AAEA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EAEA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EAEE;EAEA;;;AAGF;EACE;EAGA;;;AAIF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA;EAEE;EACA;EACA;EAEA;;;AAIF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAWF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA","file":"app.css"} \ No newline at end of file diff --git a/client/src/assets/sass/app.scss b/client/src/assets/sass/app.scss index b22b0da..9df5e09 100644 --- a/client/src/assets/sass/app.scss +++ b/client/src/assets/sass/app.scss @@ -240,7 +240,7 @@ $fontColors: ( } .subplot.active { - background-color: rgba(255, 255, 255, 0.06) !important; + background-color: $element-bg-active; } diff --git a/client/src/assets/sass/element.scss b/client/src/assets/sass/element.scss index 92ab324..7b221c1 100644 --- a/client/src/assets/sass/element.scss +++ b/client/src/assets/sass/element.scss @@ -1,12 +1,12 @@ // Copyright (c) 2022 Braden Nicholson // Static -$element-bg: hsla(214, 16, 12, 0.4); -$element-fg: hsla(214, 8, 16, 0.3); -$element-shadow: hsla(214, 16, 2, 0.1); -$element-bg-active: hsla(214, 16, 24, 0.25); +$element-bg: rgba(28, 33, 36, 0.2); +$element-fg: lighten($element-bg, 8%); +$element-bg-active: lighten($element-bg, 10%); +$element-shadow: rgba(0, 0, 0, 0.2); -$element-border: hsla(214, 16, 64, 0.07); +$element-border: lighten($element-bg, 18%); $element-border-radius: 0.25rem; @@ -17,18 +17,31 @@ $element-bg-hover: rgba(255, 128, 0, 0.280); // Options +$loadInDistance: 0.95; /* Blur Definitions */ .bg-blur { position: relative; + box-shadow: 0 0 12px 2px $element-shadow, inset 0 0 2px 1px $element-border !important; + transform: translate3d(0, 0, 0) translateZ(0); + backface-visibility: visible; -webkit-backface-visibility: hidden; -webkit-perspective: 1000px; - transform: translate3d(0, 0, 0) translateZ(0); - backface-visibility: hidden; + perspective: 1000px; - backdrop-filter: blur(24px) contrast(98%); + overflow: hidden; + content: ' '; + border-radius: inherit; + + //contrast(98%); + backdrop-filter: blur(24px); } -$loadInDistance: 0.95; +.bg-blur:before { + position: absolute; + top: 0; + right: 0; + +} @keyframes loadIn { @@ -41,25 +54,7 @@ $loadInDistance: 0.95; } // -//.bg-blur::before { -// position: absolute; -// top: 0; -// right: 0; -// -// transform: translate3d(0, 0, 0) translateZ(0); -// backface-visibility: visible; -// -webkit-backface-visibility: hidden; -// -webkit-perspective: 1000px; -// -// perspective: 1000px; -// overflow: hidden; -// content: ' '; -// border-radius: inherit; -// z-index: -1; -// width: 100%; -// height: 100%; -// //backdrop-filter: blur(24px); -//} + /* Theme Definitions */ $themes: ( @@ -76,11 +71,6 @@ $themes: ( } - .element.active { - //box-shadow: 0 0 2px 1px $element-shadow, inset 0 0 2px 1.25px $element-border; - background-color: $element-bg-active !important; - } - } } @@ -91,7 +81,7 @@ $themes: ( .element-secondary { background-color: $element-fg; - box-shadow: 0 0 8px 1px $element-shadow, inset 0 0 2px 1px $element-border; + box-shadow: 0 0 4px 0.2px opacify($element-shadow, 0.005), inset 0 0 2px 1px $element-border !important; } //.element-secondary:active { @@ -103,8 +93,10 @@ $themes: ( // Element Definition .element { + @extend .bg-blur; - box-shadow: 0 0 8px 1px $element-shadow, inset 0 0 2px 1px $element-border; + position: relative; + animation: loadIn 150ms forwards; padding: 0.25rem; border-radius: 0.5rem; @@ -148,9 +140,6 @@ $themes: ( 15% { transform: scale(0.96); } - 25% { - transform: scale(0.97); - } 100% { transform: scale(0.96); } diff --git a/client/src/components/AllocationBar.vue b/client/src/components/AllocationBar.vue index cfbdc2b..3b00341 100644 --- a/client/src/components/AllocationBar.vue +++ b/client/src/components/AllocationBar.vue @@ -1,7 +1,7 @@ @@ -95,12 +150,13 @@ function calculateLoad() { .power-chart-point { min-width: 2.75rem; border-radius: 0.25rem; - height: 4rem; + height: 3rem; width: 8px; display: flex; flex-direction: column; align-items: start; justify-content: start; - + position: relative; + padding: 0.25rem; } \ No newline at end of file diff --git a/client/src/components/IdTag.vue b/client/src/components/IdTag.vue index f3f54ca..ab1eba0 100644 --- a/client/src/components/IdTag.vue +++ b/client/src/components/IdTag.vue @@ -1,25 +1,31 @@ diff --git a/client/src/components/plot/Plot.vue b/client/src/components/plot/Plot.vue index 6b457fa..e3999ad 100644 --- a/client/src/components/plot/Plot.vue +++ b/client/src/components/plot/Plot.vue @@ -11,12 +11,10 @@ let props = defineProps() diff --git a/client/src/components/plot/Radio.vue b/client/src/components/plot/Radio.vue index c3e4563..b6c8d9d 100644 --- a/client/src/components/plot/Radio.vue +++ b/client/src/components/plot/Radio.vue @@ -66,9 +66,6 @@ function up() { 25% { transform: scale(0.98); } - 30% { - transform: scale(0.97); - } 100% { transform: scale(1); } diff --git a/client/src/components/widgets/Light.vue b/client/src/components/widgets/Light.vue index 4f9330a..66bfe8e 100644 --- a/client/src/components/widgets/Light.vue +++ b/client/src/components/widgets/Light.vue @@ -58,14 +58,14 @@ function compareOrder(a: any, b: any): number { } // Update the reactive model for the light -function updateLight(attributes: Attribute[]): void { +function updateLight(attributes: Attribute[]): Attribute[] { // Define the attributes for the light state.attributes = attributes.filter((a: Attribute) => a.entity === props.entity.id).sort(compareOrder) // Get the current power state of the light let on = state.attributes.find((a: Attribute) => a.key === 'on') let dim = state.attributes.find((a: Attribute) => a.key === 'dim') // Assign the power state to a local attribute - if (!on) return + if (!on) return [] state.powerAttribute = on as Attribute if (dim) { state.levelAttribute = dim as Attribute @@ -73,6 +73,7 @@ function updateLight(attributes: Attribute[]): void { state.active = on.value === "true" || on.request === "true" generateState() state.loading = false + return state.attributes } // Toggle the state of the context menu @@ -81,20 +82,6 @@ function toggleMenu(): void { // context(state.showMenu) } -// Apply changes made to an attribute -function togglePower() { - let newAttr = state.powerAttribute; - if (state.powerAttribute.id == "") return - newAttr.request = state.powerAttribute.value === 'true' ? 'false' : 'true' - // Make the request to the websocket object - remote.nexus.requestId("attribute", "request", newAttr, newAttr.entity) -} - -// Apply changes made to an attribute -function commitChange(attribute: Attribute) { - // Make the request to the websocket object - remote.nexus.requestId("attribute", "request", attribute, attribute.entity) -} diff --git a/client/src/components/widgets/Macros.vue b/client/src/components/widgets/Macros.vue index 965fec3..181653a 100644 --- a/client/src/components/widgets/Macros.vue +++ b/client/src/components/widgets/Macros.vue @@ -37,9 +37,7 @@ function handleUpdates(remote: Remote) { function setAttributes(key: string, value: string) { remote.attributes.filter((a: Attribute) => a.key == key && state.targets.includes(a.entity)).forEach(v => { - let copy = v - copy.request = value - remote.nexus.requestId("attribute", "request", copy, v.entity) + remote.nexus.requestAttribute(v.entity, key, value) }) } diff --git a/client/src/components/widgets/Spotify.vue b/client/src/components/widgets/Spotify.vue index 406e4d2..c6bc512 100644 --- a/client/src/components/widgets/Spotify.vue +++ b/client/src/components/widgets/Spotify.vue @@ -2,6 +2,8 @@ import moment from "moment"; import {inject, onMounted, reactive, watchEffect} from "vue"; import type {Attribute, Remote} from "@/types"; +import Plot from "@/components/plot/Plot.vue"; +import Subplot from "@/components/plot/Subplot.vue"; export interface Spotify { title: string; @@ -71,27 +73,21 @@ function updateTime() { // Apply changes made to an attribute function togglePlayback() { // Make the request to the websocket object - state.playing.request = `${state.playing.value}` - remote.nexus.requestId("attribute", "request", state.playing, state.playing.entity) + state.playing.request = `${state.playing.value === 'true' ? 'false' : 'true'}` + remote.nexus.requestAttribute(state.playing.entity, state.playing.key, state.playing.request) }