github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/tests/plugins/common.go (about)

     1  // This file is part of the Smart Home
     2  // Program complex distribution https://github.com/e154/smart-home
     3  // Copyright (C) 2016-2023, Filippov Alex
     4  //
     5  // This library is free software: you can redistribute it and/or
     6  // modify it under the terms of the GNU Lesser General Public
     7  // License as published by the Free Software Foundation; either
     8  // version 3 of the License, or (at your option) any later version.
     9  //
    10  // This library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    13  // Library General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public
    16  // License along with this library.  If not, see
    17  // <https://www.gnu.org/licenses/>.
    18  
    19  package plugins
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"go.uber.org/atomic"
    25  	"net"
    26  	"net/http"
    27  	"sync"
    28  	"time"
    29  
    30  	"github.com/phayes/freeport"
    31  	"github.com/smartystreets/goconvey/convey"
    32  
    33  	"github.com/e154/smart-home/adaptors"
    34  	"github.com/e154/smart-home/common"
    35  	"github.com/e154/smart-home/common/events"
    36  	m "github.com/e154/smart-home/models"
    37  	"github.com/e154/smart-home/plugins/alexa"
    38  	"github.com/e154/smart-home/plugins/cgminer"
    39  	"github.com/e154/smart-home/plugins/cgminer/bitmine"
    40  	"github.com/e154/smart-home/plugins/modbus_rtu"
    41  	"github.com/e154/smart-home/plugins/modbus_tcp"
    42  	"github.com/e154/smart-home/plugins/moon"
    43  	"github.com/e154/smart-home/plugins/node"
    44  	"github.com/e154/smart-home/plugins/scene"
    45  	"github.com/e154/smart-home/plugins/sun"
    46  	"github.com/e154/smart-home/plugins/telegram"
    47  	"github.com/e154/smart-home/plugins/weather"
    48  	"github.com/e154/smart-home/plugins/zigbee2mqtt"
    49  	"github.com/e154/smart-home/system/bus"
    50  	"github.com/e154/smart-home/system/scripts"
    51  )
    52  
    53  // GetNewButton ...
    54  func GetNewButton(id string, scripts []*m.Script) *m.Entity {
    55  	return &m.Entity{
    56  		Id:           common.EntityId(id),
    57  		Description:  "MiJia wireless switch",
    58  		PluginName:   zigbee2mqtt.EntityZigbee2mqtt,
    59  		Scripts:      scripts,
    60  		AutoLoad:     true,
    61  		RestoreState: true,
    62  		Attributes: m.Attributes{
    63  			"click": &m.Attribute{
    64  				Name: "click",
    65  				Type: common.AttributeString,
    66  			},
    67  			"action": &m.Attribute{
    68  				Name: "action",
    69  				Type: common.AttributeString,
    70  			},
    71  			"battery": &m.Attribute{
    72  				Name: "battery",
    73  				Type: common.AttributeInt,
    74  			},
    75  			"voltage": &m.Attribute{
    76  				Name: "voltage",
    77  				Type: common.AttributeInt,
    78  			},
    79  			"linkquality": &m.Attribute{
    80  				Name: "linkquality",
    81  				Type: common.AttributeInt,
    82  			},
    83  		},
    84  		States: []*m.EntityState{
    85  			{
    86  				Name:        "LONG_CLICK",
    87  				Description: "long click",
    88  			},
    89  			{
    90  				Name:        "LONG_ACTION",
    91  				Description: "long action",
    92  			},
    93  			{
    94  				Name:        "SINGLE_CLICK",
    95  				Description: "single click",
    96  			},
    97  			{
    98  				Name:        "SINGLE_ACTION",
    99  				Description: "single action",
   100  			},
   101  			{
   102  				Name:        "DOUBLE_CLICK",
   103  				Description: "double click",
   104  			},
   105  			{
   106  				Name:        "DOUBLE_ACTION",
   107  				Description: "double action",
   108  			},
   109  			{
   110  				Name:        "TRIPLE_CLICK",
   111  				Description: "triple click",
   112  			},
   113  			{
   114  				Name:        "TRIPLE_ACTION",
   115  				Description: "triple action",
   116  			},
   117  			{
   118  				Name:        "QUADRUPLE_CLICK",
   119  				Description: "quadruple click",
   120  			},
   121  			{
   122  				Name:        "QUADRUPLE_ACTION",
   123  				Description: "quadruple action",
   124  			},
   125  			{
   126  				Name:        "MANY_CLICK",
   127  				Description: "many click",
   128  			},
   129  			{
   130  				Name:        "MANY_ACTION",
   131  				Description: "many action",
   132  			},
   133  			{
   134  				Name:        "LONG_RELEASE_CLICK",
   135  				Description: "long_release click",
   136  			},
   137  			{
   138  				Name:        "HOLD_ACTION",
   139  				Description: "hold action",
   140  			},
   141  			{
   142  				Name:        "RELEASE_ACTION",
   143  				Description: "release action",
   144  			},
   145  		},
   146  	}
   147  }
   148  
   149  // GetNewPlug ...
   150  func GetNewPlug(id string, scrits []*m.Script) *m.Entity {
   151  	return &m.Entity{
   152  		Id:           common.EntityId(id),
   153  		Description:  "MiJia power plug ZigBee",
   154  		PluginName:   zigbee2mqtt.EntityZigbee2mqtt,
   155  		Scripts:      scrits,
   156  		AutoLoad:     true,
   157  		RestoreState: true,
   158  		Attributes: m.Attributes{
   159  			"power": &m.Attribute{
   160  				Name: "power",
   161  				Type: common.AttributeInt,
   162  			},
   163  			"state": &m.Attribute{
   164  				Name: "state",
   165  				Type: common.AttributeString,
   166  			},
   167  			"voltage": &m.Attribute{
   168  				Name: "voltage",
   169  				Type: common.AttributeInt,
   170  			},
   171  			"consumption": &m.Attribute{
   172  				Name: "consumption",
   173  				Type: common.AttributeString,
   174  			},
   175  			"linkquality": &m.Attribute{
   176  				Name: "linkquality",
   177  				Type: common.AttributeInt,
   178  			},
   179  			"temperature": &m.Attribute{
   180  				Name: "temperature",
   181  				Type: common.AttributeInt,
   182  			},
   183  		},
   184  		States: []*m.EntityState{
   185  			{
   186  				Name:        "ON",
   187  				Description: "on state",
   188  			},
   189  			{
   190  				Name:        "OFF",
   191  				Description: "off state",
   192  			},
   193  		},
   194  	}
   195  }
   196  
   197  // GetNewScene ...
   198  func GetNewScene(id string, scripts []*m.Script) *m.Entity {
   199  	return &m.Entity{
   200  		Id:           common.EntityId(id),
   201  		Description:  "scene",
   202  		PluginName:   scene.EntityScene,
   203  		Scripts:      scripts,
   204  		AutoLoad:     true,
   205  		RestoreState: true,
   206  	}
   207  }
   208  
   209  // GetNewNode ...
   210  func GetNewNode(name string) *m.Entity {
   211  	settings := node.NewSettings()
   212  	settings[node.AttrNodeLogin].Value = "node1"
   213  	settings[node.AttrNodePass].Value = "node1"
   214  	return &m.Entity{
   215  		Id:           common.EntityId(fmt.Sprintf("node.%s", name)),
   216  		Description:  "main node",
   217  		PluginName:   "node",
   218  		AutoLoad:     true,
   219  		RestoreState: true,
   220  		Attributes:   node.NewAttr(),
   221  		Settings:     settings,
   222  	}
   223  }
   224  
   225  // GetNewMoon ...
   226  func GetNewMoon(name string) *m.Entity {
   227  	settings := moon.NewSettings()
   228  	settings[moon.AttrLat].Value = 54.9022
   229  	settings[moon.AttrLon].Value = 83.0335
   230  	return &m.Entity{
   231  		Id:           common.EntityId(fmt.Sprintf("moon.%s", name)),
   232  		Description:  "home",
   233  		PluginName:   "moon",
   234  		AutoLoad:     true,
   235  		RestoreState: true,
   236  		Attributes:   moon.NewAttr(),
   237  		Settings:     settings,
   238  		States: []*m.EntityState{
   239  			{
   240  				Name:        moon.StateAboveHorizon,
   241  				Description: "above horizon",
   242  			},
   243  			{
   244  				Name:        moon.StateBelowHorizon,
   245  				Description: "below horizon",
   246  			},
   247  		},
   248  	}
   249  }
   250  
   251  // GetNewWeatherMet ...
   252  func GetNewWeatherMet(name string) *m.Entity {
   253  	settings := weather.NewSettings()
   254  	settings[weather.AttrLat].Value = 54.9022
   255  	settings[weather.AttrLon].Value = 83.0335
   256  	return &m.Entity{
   257  		Id:           common.EntityId(fmt.Sprintf("weather_met.%s", name)),
   258  		Description:  name,
   259  		PluginName:   "weather_met",
   260  		AutoLoad:     true,
   261  		RestoreState: true,
   262  		Attributes:   weather.BaseForecast(),
   263  		Settings:     settings,
   264  	}
   265  }
   266  
   267  // GetNewWeatherOwm ...
   268  //func GetNewWeatherOwm(name string) *m.Entity {
   269  //	settings := weather_owm.NewSettings()
   270  //	settings[weather_owm.AttrAppid].Value = "**************"
   271  //	settings[weather_owm.AttrUnits].Value = "metric"
   272  //	settings[weather_owm.AttrLang].Value = "ru"
   273  //	return &m.Entity{
   274  //		Id:          common.EntityId(fmt.Sprintf("weather_owm.%s", name)),
   275  //		Description: "weather owm",
   276  //		PluginName:  weather_owm.EntityWeatherOwm,
   277  //		AutoLoad:    true,
   278  //		Settings:    settings,
   279  //	}
   280  //}
   281  
   282  // GetNewSun ...
   283  func GetNewSun(name string) *m.Entity {
   284  	settings := sun.NewSettings()
   285  	settings[sun.AttrLat].Value = 54.9022
   286  	settings[sun.AttrLon].Value = 83.0335
   287  	return &m.Entity{
   288  		Id:           common.EntityId(fmt.Sprintf("sun.%s", name)),
   289  		Description:  "home",
   290  		PluginName:   "sun",
   291  		AutoLoad:     true,
   292  		RestoreState: true,
   293  		Attributes:   sun.NewAttr(),
   294  		Settings:     settings,
   295  		States: []*m.EntityState{
   296  			{
   297  				Name:        sun.AttrDusk,
   298  				Description: "dusk (evening nautical twilight starts)",
   299  			},
   300  		},
   301  	}
   302  }
   303  
   304  // GetNewBitmineL3 ...
   305  func GetNewBitmineL3(name string) *m.Entity {
   306  	settings := cgminer.NewSettings()
   307  	settings[cgminer.SettingHost].Value = "192.168.0.243"
   308  	settings[cgminer.SettingPort].Value = 4028
   309  	settings[cgminer.SettingTimeout].Value = 2
   310  	settings[cgminer.SettingUser].Value = "user"
   311  	settings[cgminer.SettingPass].Value = "pass"
   312  	settings[cgminer.SettingManufacturer].Value = bitmine.ManufactureBitmine
   313  	settings[cgminer.SettingModel].Value = bitmine.DeviceL3Plus
   314  	return &m.Entity{
   315  		Id:           common.EntityId(fmt.Sprintf("cgminer.%s", name)),
   316  		Description:  "antminer L3",
   317  		PluginName:   "cgminer",
   318  		AutoLoad:     true,
   319  		RestoreState: true,
   320  		Attributes:   cgminer.NewAttr(),
   321  		Settings:     settings,
   322  	}
   323  }
   324  
   325  // GetNewSensor ...
   326  func GetNewSensor(name string) *m.Entity {
   327  
   328  	return &m.Entity{
   329  		Id:           common.EntityId(fmt.Sprintf("sensor.%s", name)),
   330  		Description:  "api",
   331  		PluginName:   "sensor",
   332  		AutoLoad:     true,
   333  		RestoreState: true,
   334  	}
   335  }
   336  
   337  // GetNewModbusRtu ...
   338  func GetNewModbusRtu(name string) *m.Entity {
   339  	return &m.Entity{
   340  		Id:           common.EntityId(fmt.Sprintf("modbus_rtu.%s", name)),
   341  		Description:  fmt.Sprintf("%s entity", name),
   342  		PluginName:   "modbus_rtu",
   343  		AutoLoad:     true,
   344  		RestoreState: true,
   345  		Attributes:   modbus_rtu.NewAttr(),
   346  		Settings:     modbus_rtu.NewSettings(),
   347  	}
   348  }
   349  
   350  // GetNewModbusTcp ...
   351  func GetNewModbusTcp(name string) *m.Entity {
   352  	return &m.Entity{
   353  		Id:           common.EntityId(fmt.Sprintf("modbus_tcp.%s", name)),
   354  		Description:  fmt.Sprintf("%s entity", name),
   355  		PluginName:   "modbus_tcp",
   356  		AutoLoad:     true,
   357  		RestoreState: true,
   358  		Attributes:   modbus_tcp.NewAttr(),
   359  		Settings:     modbus_tcp.NewSettings(),
   360  	}
   361  }
   362  
   363  // GetNewTelegram ...
   364  func GetNewTelegram(name string) *m.Entity {
   365  	settings := telegram.NewSettings()
   366  	settings[telegram.AttrToken].Value = "XXXX"
   367  	return &m.Entity{
   368  		Id:           common.EntityId(fmt.Sprintf("%s.%s", telegram.Name, name)),
   369  		Description:  "",
   370  		PluginName:   telegram.Name,
   371  		AutoLoad:     true,
   372  		RestoreState: true,
   373  		Attributes:   telegram.NewAttr(),
   374  		Settings:     settings,
   375  	}
   376  }
   377  
   378  // AddPlugin ...
   379  func AddPlugin(adaptors *adaptors.Adaptors, name string, opts ...m.AttributeValue) (err error) {
   380  	plugin := &m.Plugin{
   381  		Name:    name,
   382  		Version: "0.0.1",
   383  		Enabled: true,
   384  		System:  true,
   385  	}
   386  	if len(opts) > 0 {
   387  		plugin.Settings = opts[0]
   388  	}
   389  	err = adaptors.Plugin.CreateOrUpdate(context.Background(), plugin)
   390  	return
   391  }
   392  
   393  // RegisterConvey ...
   394  func RegisterConvey(scriptService scripts.ScriptService, ctx convey.C) {
   395  	scriptService.PushFunctions("So", func(actual interface{}, assert string, expected interface{}) {
   396  		//fmt.Printf("actual(%v), expected(%v)\n", actual, expected)
   397  		switch assert {
   398  		case "ShouldEqual":
   399  			ctx.So(fmt.Sprintf("%v", actual), convey.ShouldEqual, expected)
   400  		case "ShouldNotBeBlank":
   401  			ctx.So(fmt.Sprintf("%v", actual), convey.ShouldNotBeBlank)
   402  		}
   403  	})
   404  
   405  }
   406  
   407  // Wait ...
   408  func Wait(timeOut time.Duration, ch chan interface{}) (ok bool) {
   409  
   410  	select {
   411  	case <-ch:
   412  		ok = true
   413  	case <-time.After(timeOut):
   414  	}
   415  	return
   416  }
   417  
   418  // WaitT ...
   419  func WaitT[T events.EventTriggerLoaded | events.EventTriggerCompleted | events.EventStateChanged | alexa.EventAlexaAction | []byte | struct{}](timeOut time.Duration, ch chan T) (v T, ok bool) {
   420  
   421  	select {
   422  	case v = <-ch:
   423  		ok = true
   424  		return
   425  	case <-time.After(timeOut):
   426  	}
   427  	return
   428  }
   429  
   430  type accepted struct {
   431  	conn net.Conn
   432  	err  error
   433  }
   434  
   435  // MockHttpServer ...
   436  func MockHttpServer(ctx context.Context, ip string, port int64, payload []byte) (err error) {
   437  
   438  	var listener net.Listener
   439  	if listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port)); err != nil {
   440  		return
   441  	}
   442  
   443  	_ = http.Serve(listener, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   444  		rw.WriteHeader(200)
   445  		_, _ = fmt.Fprintf(rw, string(payload))
   446  	}))
   447  	c := make(chan accepted, 1)
   448  	for {
   449  		select {
   450  		case <-ctx.Done():
   451  			_ = listener.Close()
   452  			return
   453  		case a := <-c:
   454  			if a.err != nil {
   455  				err = a.err
   456  				return
   457  			}
   458  			go func(conn net.Conn) {
   459  				_, _ = conn.Write(payload)
   460  				_ = conn.Close()
   461  			}(a.conn)
   462  		default:
   463  		}
   464  	}
   465  }
   466  
   467  // MockTCPServer ...
   468  func MockTCPServer(ctx context.Context, ip string, port int64, payloads ...[]byte) (err error) {
   469  	var listener net.Listener
   470  	if listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port)); err != nil {
   471  		return
   472  	}
   473  	c := make(chan accepted, 3)
   474  	go func() {
   475  		for {
   476  			conn, err := listener.Accept()
   477  			c <- accepted{conn, err}
   478  		}
   479  	}()
   480  	var counter int
   481  	for {
   482  		select {
   483  		case <-ctx.Done():
   484  			_ = listener.Close()
   485  			return
   486  		case a := <-c:
   487  			if a.err != nil {
   488  				err = a.err
   489  				return
   490  			}
   491  			go func(conn net.Conn) {
   492  				if counter < len(payloads) {
   493  					_, _ = conn.Write(payloads[counter])
   494  				} else {
   495  					_, _ = conn.Write(payloads[len(payloads)-1])
   496  				}
   497  				_ = conn.Close()
   498  				counter++
   499  			}(a.conn)
   500  		default:
   501  		}
   502  	}
   503  }
   504  
   505  // GetPort ...
   506  func GetPort() int64 {
   507  	port, _ := freeport.GetFreePort()
   508  	return int64(port)
   509  }
   510  
   511  // AddScript ...
   512  func AddScript(name, src string, adaptors *adaptors.Adaptors, scriptService scripts.ScriptService) (script *m.Script, err error) {
   513  
   514  	script = &m.Script{
   515  		Lang:        common.ScriptLangCoffee,
   516  		Name:        name,
   517  		Source:      src,
   518  		Description: "description " + name,
   519  	}
   520  
   521  	var engine *scripts.Engine
   522  	if engine, err = scriptService.NewEngine(script); err != nil {
   523  		return
   524  	}
   525  
   526  	if err = engine.Compile(); err != nil {
   527  		return
   528  	}
   529  
   530  	script.Id, err = adaptors.Script.Add(context.Background(), script)
   531  
   532  	return
   533  }
   534  
   535  func AddTrigger(trigger *m.NewTrigger, adaptors *adaptors.Adaptors, eventBus bus.Bus) (id int64, err error) {
   536  	if id, err = adaptors.Trigger.Add(context.Background(), trigger); err != nil {
   537  		return
   538  	}
   539  	eventBus.Publish(fmt.Sprintf("system/automation/triggers/%d", id), events.EventCreatedTriggerModel{
   540  		Id: id,
   541  	})
   542  	return
   543  }
   544  
   545  func AddTask(newTask *m.NewTask, adaptors *adaptors.Adaptors, eventBus bus.Bus) (task1Id int64, err error) {
   546  	if task1Id, err = adaptors.Task.Add(context.Background(), newTask); err != nil {
   547  		return
   548  	}
   549  	eventBus.Publish(fmt.Sprintf("system/automation/tasks/%d", task1Id), events.EventCreatedTaskModel{
   550  		Id: task1Id,
   551  	})
   552  	return
   553  }
   554  
   555  func WaitTask(eventBus bus.Bus, timeOut time.Duration, tasks ...int64) (result chan bool) {
   556  
   557  	list := map[int64]bool{}
   558  	for _, task := range tasks {
   559  		list[task] = false
   560  	}
   561  
   562  	var closed = atomic.NewBool(false)
   563  	result = make(chan bool, 1)
   564  	go func() {
   565  		mx := sync.Mutex{}
   566  
   567  		ch := make(chan interface{})
   568  		defer close(ch)
   569  		fn := func(_ string, msg interface{}) {
   570  			switch v := msg.(type) {
   571  			case events.EventTaskLoaded:
   572  				mx.Lock()
   573  				defer mx.Unlock()
   574  				fmt.Printf("Task %d loaded ...\r\n", v.Id)
   575  				if _, ok := list[v.Id]; ok {
   576  					list[v.Id] = true
   577  				} else {
   578  					return
   579  				}
   580  				for _, loaded := range list {
   581  					if !loaded {
   582  						return
   583  					}
   584  				}
   585  				if closed.Load() {
   586  					return
   587  				}
   588  				ch <- struct{}{}
   589  			}
   590  
   591  		}
   592  		eventBus.Subscribe("system/automation/tasks/+", fn, true)
   593  		defer eventBus.Unsubscribe("system/automation/tasks/+", fn)
   594  
   595  		result <- Wait(timeOut, ch)
   596  		closed.Store(true)
   597  		close(result)
   598  
   599  	}()
   600  
   601  	return
   602  }
   603  
   604  func WaitEntity(eventBus bus.Bus, timeOut time.Duration, entities ...string) (result chan bool) {
   605  
   606  	list := map[string]bool{}
   607  	for _, entity := range entities {
   608  		list[entity] = false
   609  	}
   610  
   611  	var closed = atomic.NewBool(false)
   612  	result = make(chan bool, 1)
   613  	go func() {
   614  		mx := sync.Mutex{}
   615  
   616  		ch := make(chan interface{})
   617  		defer close(ch)
   618  		fn := func(_ string, msg interface{}) {
   619  			switch v := msg.(type) {
   620  			case events.EventEntityLoaded:
   621  				mx.Lock()
   622  				defer mx.Unlock()
   623  				fmt.Printf("Plugin %s loaded ...\r\n", v.EntityId.String())
   624  				if _, ok := list[v.EntityId.String()]; ok {
   625  					list[v.EntityId.String()] = true
   626  				} else {
   627  					return
   628  				}
   629  				for _, loaded := range list {
   630  					if !loaded {
   631  						return
   632  					}
   633  				}
   634  				if closed.Load() {
   635  					return
   636  				}
   637  				ch <- struct{}{}
   638  			}
   639  
   640  		}
   641  		eventBus.Subscribe("system/entities/+", fn, true)
   642  		defer eventBus.Unsubscribe("system/entities/+", fn)
   643  
   644  		result <- Wait(timeOut, ch)
   645  		closed.Store(true)
   646  		close(result)
   647  
   648  	}()
   649  
   650  	return
   651  }
   652  
   653  func WaitPlugins(eventBus bus.Bus, timeOut time.Duration, plugins ...string) (result chan bool) {
   654  
   655  	list := map[string]bool{}
   656  	for _, plugin := range plugins {
   657  		list[plugin] = false
   658  	}
   659  
   660  	var closed = atomic.NewBool(false)
   661  	result = make(chan bool, 1)
   662  	go func() {
   663  		mx := sync.Mutex{}
   664  
   665  		ch := make(chan interface{})
   666  		defer close(ch)
   667  		fn := func(_ string, msg interface{}) {
   668  			switch v := msg.(type) {
   669  			case events.EventPluginLoaded:
   670  				mx.Lock()
   671  				defer mx.Unlock()
   672  				fmt.Printf("Plugin %s loaded ...\r\n", v.PluginName)
   673  				if _, ok := list[v.PluginName]; ok {
   674  					list[v.PluginName] = true
   675  				} else {
   676  					return
   677  				}
   678  				for _, loaded := range list {
   679  					if !loaded {
   680  						return
   681  					}
   682  				}
   683  				if closed.Load() {
   684  					return
   685  				}
   686  				ch <- struct{}{}
   687  			}
   688  
   689  		}
   690  		eventBus.Subscribe("system/plugins/+", fn, true)
   691  		defer eventBus.Unsubscribe("system/plugins/+", fn)
   692  
   693  		result <- Wait(timeOut, ch)
   694  		closed.Store(true)
   695  		close(result)
   696  
   697  	}()
   698  
   699  	return
   700  }
   701  
   702  func WaitService(eventBus bus.Bus, timeOut time.Duration, services ...string) (result chan bool) {
   703  
   704  	list := map[string]bool{}
   705  	for _, service := range services {
   706  		list[service] = false
   707  	}
   708  
   709  	var closed = atomic.NewBool(false)
   710  	result = make(chan bool, 1)
   711  	go func() {
   712  		mx := sync.Mutex{}
   713  
   714  		ch := make(chan interface{})
   715  		defer close(ch)
   716  		fn := func(_ string, msg interface{}) {
   717  			switch v := msg.(type) {
   718  			case events.EventServiceStarted:
   719  				mx.Lock()
   720  				defer mx.Unlock()
   721  				fmt.Printf("Service %s started ...\r\n", v.Service)
   722  				if _, ok := list[v.Service]; ok {
   723  					list[v.Service] = true
   724  				} else {
   725  					return
   726  				}
   727  				for _, started := range list {
   728  					if !started {
   729  						return
   730  					}
   731  				}
   732  				if closed.Load() {
   733  					return
   734  				}
   735  				ch <- struct{}{}
   736  			}
   737  
   738  		}
   739  		eventBus.Subscribe("system/services/+", fn, true)
   740  		defer eventBus.Unsubscribe("system/services/+", fn)
   741  
   742  		time.Sleep(time.Millisecond * 500)
   743  
   744  		result <- Wait(timeOut, ch)
   745  		closed.Store(true)
   746  		close(result)
   747  
   748  	}()
   749  
   750  	return
   751  }
   752  
   753  func WaitMessage[T events.EventStateChanged | events.EventTriggerCompleted | events.EventTriggerLoaded](
   754  	eventBus bus.Bus, timeOut time.Duration, topic string, options ...interface{},
   755  ) (msg T, ok bool) {
   756  
   757  	var closed = atomic.NewBool(false)
   758  	ch := make(chan T)
   759  	defer close(ch)
   760  	fn := func(_ string, msg interface{}) {
   761  		switch v := msg.(type) {
   762  		case T:
   763  			if closed.Load() {
   764  				return
   765  			}
   766  			ch <- v
   767  		}
   768  	}
   769  
   770  	var retain bool
   771  	if len(options) > 0 {
   772  		retain, _ = options[0].(bool)
   773  	}
   774  
   775  	eventBus.Subscribe(topic, fn, retain)
   776  	defer eventBus.Unsubscribe(topic, fn)
   777  	defer closed.Store(true)
   778  
   779  	msg, ok = WaitT[T](timeOut, ch)
   780  
   781  	time.Sleep(time.Millisecond * 500)
   782  
   783  	return
   784  }
   785  
   786  func WaitGroupTimeout(wg *sync.WaitGroup, timeOut time.Duration) bool {
   787  	ch := make(chan struct{})
   788  	go func() {
   789  		defer close(ch)
   790  		wg.Wait()
   791  	}()
   792  	select {
   793  	case <-ch:
   794  		return true
   795  	case <-time.After(timeOut):
   796  		return false
   797  	}
   798  }