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 }