github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/tests/plugins/modbus_tcp/modbus_tcp_test.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 modbus_tcp
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/e154/smart-home/common/events"
    29  
    30  	"github.com/e154/smart-home/adaptors"
    31  	m "github.com/e154/smart-home/models"
    32  	"github.com/e154/smart-home/plugins/modbus_tcp"
    33  	"github.com/e154/smart-home/plugins/node"
    34  	"github.com/e154/smart-home/system/automation"
    35  	"github.com/e154/smart-home/system/bus"
    36  	"github.com/e154/smart-home/system/migrations"
    37  	"github.com/e154/smart-home/system/mqtt"
    38  	"github.com/e154/smart-home/system/scripts"
    39  	"github.com/e154/smart-home/system/supervisor"
    40  	. "github.com/e154/smart-home/tests/plugins"
    41  	. "github.com/smartystreets/goconvey/convey"
    42  )
    43  
    44  func TestModbusTcp(t *testing.T) {
    45  
    46  	const plugActionOnOffSourceScript = `
    47  
    48  writeRegisters =(d, c, r)->
    49      return ModbusTcp 'WriteMultipleRegisters', d, c, r
    50  
    51  checkStatus =->
    52      COMMAND = []
    53      FUNC = 'ReadHoldingRegisters'
    54      ADDRESS = 0
    55      COUNT = 16
    56  
    57      res = ModbusTcp FUNC, ADDRESS, COUNT, COMMAND
    58      So(res.error, 'ShouldEqual', '')
    59      So(res.result, 'ShouldEqual', '[1 0 1]')
    60      So(res.time, 'ShouldNotBeBlank', '')
    61  
    62  doOnAction = ->
    63      res = writeRegisters(0, 1, [1])
    64      So(res.error, 'ShouldEqual', '')
    65      So(res.result, 'ShouldEqual', '[]')
    66      So(res.time, 'ShouldNotBeBlank', '')
    67  
    68  doOnErrAction = ->
    69      res = writeRegisters(0, 1, [1])
    70      So(res.error, 'ShouldEqual', 'some error')
    71      So(res.result, 'ShouldEqual', '[]')
    72      So(res.time, 'ShouldNotBeBlank', '')
    73  
    74  doOffAction = ->
    75      res = writeRegisters(0, 1, [0])
    76      So(res.error, 'ShouldEqual', '')
    77      So(res.result, 'ShouldEqual', '[]')
    78      So(res.time, 'ShouldNotBeBlank', '')
    79  
    80  entityAction = (entityId, actionName)->
    81      #print '---action on/off--'
    82      switch actionName
    83          when 'ON' then doOnAction()
    84          when 'OFF' then doOffAction()
    85          when 'CHECK' then checkStatus()
    86          when 'ON_WITH_ERR' then doOnErrAction()
    87  `
    88  
    89  	Convey("modbus_tcp", t, func(ctx C) {
    90  		_ = container.Invoke(func(adaptors *adaptors.Adaptors,
    91  			migrations *migrations.Migrations,
    92  			scriptService scripts.ScriptService,
    93  			supervisor supervisor.Supervisor,
    94  			mqttServer mqtt.MqttServ,
    95  			automation automation.Automation,
    96  			eventBus bus.Bus) {
    97  
    98  			// register plugins
    99  			AddPlugin(adaptors, "node")
   100  			AddPlugin(adaptors, "triggers")
   101  			AddPlugin(adaptors, "modbus_tcp")
   102  
   103  			serviceCh := WaitService(eventBus, time.Second*5, "Supervisor", "Automation", "Mqtt")
   104  			pluginsCh := WaitPlugins(eventBus, time.Second*5, "triggers", "modbus_tcp", "node")
   105  			automation.Start()
   106  			go mqttServer.Start()
   107  			supervisor.Start(context.Background())
   108  			defer automation.Shutdown()
   109  			defer mqttServer.Shutdown()
   110  			defer supervisor.Shutdown(context.Background())
   111  			So(<-serviceCh, ShouldBeTrue)
   112  			So(<-pluginsCh, ShouldBeTrue)
   113  
   114  			// bind convey
   115  			RegisterConvey(scriptService, ctx)
   116  
   117  			// add scripts
   118  			// ------------------------------------------------
   119  
   120  			plugActionOnOffScript, err := AddScript("plug script", plugActionOnOffSourceScript, adaptors, scriptService)
   121  			So(err, ShouldBeNil)
   122  
   123  			// add entity
   124  			// ------------------------------------------------
   125  			nodeEnt := GetNewNode("second")
   126  			err = adaptors.Entity.Add(context.Background(), nodeEnt)
   127  			So(err, ShouldBeNil)
   128  
   129  			plugEnt := GetNewModbusTcp("plug")
   130  			plugEnt.ParentId = &nodeEnt.Id
   131  			plugEnt.Actions = []*m.EntityAction{
   132  				{
   133  					Name:        "ON",
   134  					Description: "включить",
   135  					Script:      plugActionOnOffScript,
   136  				},
   137  				{
   138  					Name:        "OFF",
   139  					Description: "выключить",
   140  					Script:      plugActionOnOffScript,
   141  				},
   142  				{
   143  					Name:        "CHECK",
   144  					Description: "condition check",
   145  					Script:      plugActionOnOffScript,
   146  				},
   147  				{
   148  					Name:        "ON_WITH_ERR",
   149  					Description: "error case",
   150  					Script:      plugActionOnOffScript,
   151  				},
   152  			}
   153  			plugEnt.States = []*m.EntityState{
   154  				{
   155  					Name:        "ON",
   156  					Description: "on state",
   157  				},
   158  				{
   159  					Name:        "OFF",
   160  					Description: "off state",
   161  				},
   162  				{
   163  					Name:        "ERROR",
   164  					Description: "error state",
   165  				},
   166  			}
   167  			plugEnt.Settings[modbus_tcp.AttrSlaveId].Value = 1
   168  			plugEnt.Settings[modbus_tcp.AttrAddressPort].Value = "office:502"
   169  			err = adaptors.Entity.Add(context.Background(), plugEnt)
   170  			So(err, ShouldBeNil)
   171  			_, err = adaptors.EntityStorage.Add(context.Background(), &m.EntityStorage{
   172  				EntityId:   plugEnt.Id,
   173  				Attributes: plugEnt.Attributes.Serialize(),
   174  			})
   175  			So(err, ShouldBeNil)
   176  
   177  			eventBus.Publish("system/models/entities/"+nodeEnt.Id.String(), events.EventCreatedEntityModel{
   178  				EntityId: nodeEnt.Id,
   179  			})
   180  			eventBus.Publish("system/models/entities/"+nodeEnt.Id.String(), events.EventCreatedEntityModel{
   181  				EntityId: plugEnt.Id,
   182  			})
   183  
   184  			time.Sleep(time.Second)
   185  
   186  			// ------------------------------------------------
   187  
   188  			ch := make(chan []byte)
   189  			mqttCli := mqttServer.NewClient("cli")
   190  			_ = mqttCli.Subscribe("system/plugins/node/second/req/#", func(cli mqtt.MqttCli, message mqtt.Message) {
   191  				ch <- message.Payload
   192  			})
   193  			defer mqttCli.UnsubscribeAll()
   194  
   195  			// commands
   196  			t.Run("on command", func(t *testing.T) {
   197  				Convey("stats", t, func(ctx C) {
   198  					supervisor.CallAction(plugEnt.Id, "ON", nil)
   199  
   200  					// wait message
   201  					req, ok := WaitT[[]byte](time.Second*2, ch)
   202  					ctx.So(ok, ShouldBeTrue)
   203  
   204  					// what see node
   205  					request := node.MessageRequest{}
   206  					err = json.Unmarshal(req, &request)
   207  					ctx.So(err, ShouldBeNil)
   208  
   209  					cmd := modbus_tcp.ModBusCommand{}
   210  					err = json.Unmarshal(request.Command, &cmd)
   211  					ctx.So(err, ShouldBeNil)
   212  
   213  					prop := map[string]interface{}{}
   214  					err = json.Unmarshal(request.Properties, &prop)
   215  					ctx.So(err, ShouldBeNil)
   216  
   217  					ctx.So(request.EntityId, ShouldEqual, plugEnt.Id)
   218  					ctx.So(request.DeviceType, ShouldEqual, modbus_tcp.DeviceTypeModbusTcp)
   219  					ctx.So(prop["slave_id"], ShouldEqual, 1)
   220  					ctx.So(prop["address_port"], ShouldEqual, "office:502")
   221  
   222  					ctx.So(cmd.Function, ShouldEqual, "WriteMultipleRegisters")
   223  					ctx.So(cmd.Address, ShouldEqual, 0)
   224  					ctx.So(cmd.Count, ShouldEqual, 1)
   225  					ctx.So(cmd.Command, ShouldResemble, []uint16{1})
   226  
   227  					// response from node
   228  					r := modbus_tcp.ModBusResponse{
   229  						Error:  "",
   230  						Result: []uint16{},
   231  					}
   232  					b, _ := json.Marshal(r)
   233  					resp := node.MessageResponse{
   234  						EntityId:   plugEnt.Id,
   235  						DeviceType: modbus_tcp.DeviceTypeModbusTcp,
   236  						Properties: nil,
   237  						Response:   b,
   238  						Status:     "",
   239  					}
   240  					b, _ = json.Marshal(resp)
   241  					_ = mqttCli.Publish("system/plugins/node/second/resp/plugin.test", b)
   242  					_ = mqttCli.Publish(fmt.Sprintf("system/plugins/node/second/resp/%s", plugEnt.Id), b)
   243  
   244  					time.Sleep(time.Millisecond * 500)
   245  				})
   246  			})
   247  
   248  			t.Run("off command", func(t *testing.T) {
   249  				Convey("stats", t, func(ctx C) {
   250  					supervisor.CallAction(plugEnt.Id, "OFF", nil)
   251  
   252  					// wait message
   253  					req, ok := WaitT[[]byte](time.Second*2, ch)
   254  					ctx.So(ok, ShouldBeTrue)
   255  
   256  					// what see node
   257  					request := node.MessageRequest{}
   258  					err = json.Unmarshal(req, &request)
   259  					ctx.So(err, ShouldBeNil)
   260  
   261  					cmd := modbus_tcp.ModBusCommand{}
   262  					err = json.Unmarshal(request.Command, &cmd)
   263  					ctx.So(err, ShouldBeNil)
   264  
   265  					prop := map[string]interface{}{}
   266  					err = json.Unmarshal(request.Properties, &prop)
   267  					ctx.So(err, ShouldBeNil)
   268  
   269  					ctx.So(request.EntityId, ShouldEqual, plugEnt.Id)
   270  					ctx.So(request.DeviceType, ShouldEqual, modbus_tcp.DeviceTypeModbusTcp)
   271  					ctx.So(prop["slave_id"], ShouldEqual, 1)
   272  					ctx.So(prop["address_port"], ShouldEqual, "office:502")
   273  
   274  					ctx.So(cmd.Function, ShouldEqual, "WriteMultipleRegisters")
   275  					ctx.So(cmd.Address, ShouldEqual, 0)
   276  					ctx.So(cmd.Count, ShouldEqual, 1)
   277  					ctx.So(cmd.Command, ShouldResemble, []uint16{0})
   278  
   279  					// response from node
   280  					r := modbus_tcp.ModBusResponse{
   281  						Error:  "",
   282  						Result: []uint16{},
   283  					}
   284  					b, _ := json.Marshal(r)
   285  					resp := node.MessageResponse{
   286  						EntityId:   plugEnt.Id,
   287  						DeviceType: modbus_tcp.DeviceTypeModbusTcp,
   288  						Properties: nil,
   289  						Response:   b,
   290  						Status:     "",
   291  					}
   292  					b, _ = json.Marshal(resp)
   293  					_ = mqttCli.Publish("system/plugins/node/second/resp/plugin.test", b)
   294  					_ = mqttCli.Publish(fmt.Sprintf("system/plugins/node/second/resp/%s", plugEnt.Id), b)
   295  
   296  					time.Sleep(time.Millisecond * 500)
   297  				})
   298  			})
   299  
   300  			t.Run("check command", func(t *testing.T) {
   301  				Convey("stats", t, func(ctx C) {
   302  					supervisor.CallAction(plugEnt.Id, "CHECK", nil)
   303  
   304  					// wait message
   305  					req, ok := WaitT[[]byte](time.Second*2, ch)
   306  					ctx.So(ok, ShouldBeTrue)
   307  
   308  					// what see node
   309  					request := node.MessageRequest{}
   310  					err = json.Unmarshal(req, &request)
   311  					ctx.So(err, ShouldBeNil)
   312  
   313  					cmd := modbus_tcp.ModBusCommand{}
   314  					err = json.Unmarshal(request.Command, &cmd)
   315  					ctx.So(err, ShouldBeNil)
   316  
   317  					prop := map[string]interface{}{}
   318  					err = json.Unmarshal(request.Properties, &prop)
   319  					ctx.So(err, ShouldBeNil)
   320  
   321  					ctx.So(request.EntityId, ShouldEqual, plugEnt.Id)
   322  					ctx.So(request.DeviceType, ShouldEqual, modbus_tcp.DeviceTypeModbusTcp)
   323  					ctx.So(prop["slave_id"], ShouldEqual, 1)
   324  					ctx.So(prop["address_port"], ShouldEqual, "office:502")
   325  
   326  					ctx.So(cmd.Function, ShouldEqual, "ReadHoldingRegisters")
   327  					ctx.So(cmd.Address, ShouldEqual, 0)
   328  					ctx.So(cmd.Count, ShouldEqual, 16)
   329  					ctx.So(cmd.Command, ShouldResemble, []uint16{})
   330  
   331  					// response from node
   332  					r := modbus_tcp.ModBusResponse{
   333  						Error:  "",
   334  						Result: []uint16{1, 0, 1},
   335  					}
   336  					b, _ := json.Marshal(r)
   337  					resp := node.MessageResponse{
   338  						EntityId:   plugEnt.Id,
   339  						DeviceType: modbus_tcp.DeviceTypeModbusTcp,
   340  						Properties: nil,
   341  						Response:   b,
   342  						Status:     "",
   343  					}
   344  					b, _ = json.Marshal(resp)
   345  					_ = mqttCli.Publish("system/plugins/node/second/resp/plugin.test", b)
   346  					_ = mqttCli.Publish(fmt.Sprintf("system/plugins/node/second/resp/%s", plugEnt.Id), b)
   347  
   348  					time.Sleep(time.Millisecond * 500)
   349  				})
   350  			})
   351  
   352  			t.Run("bad command", func(t *testing.T) {
   353  				Convey("stats", t, func(ctx C) {
   354  					supervisor.CallAction(plugEnt.Id, "NULL", nil)
   355  
   356  					// wait message
   357  					_, ok := WaitT[[]byte](time.Second*2, ch)
   358  					ctx.So(ok, ShouldBeFalse)
   359  				})
   360  			})
   361  
   362  			t.Run("response with error", func(t *testing.T) {
   363  				Convey("stats", t, func(ctx C) {
   364  					supervisor.CallAction(plugEnt.Id, "ON_WITH_ERR", nil)
   365  
   366  					// wait message
   367  					_, ok := WaitT[[]byte](time.Second*2, ch)
   368  					ctx.So(ok, ShouldBeTrue)
   369  
   370  					r := modbus_tcp.ModBusResponse{
   371  						Error: "some error",
   372  					}
   373  					b, _ := json.Marshal(r)
   374  					resp := node.MessageResponse{
   375  						EntityId:   plugEnt.Id,
   376  						DeviceType: modbus_tcp.DeviceTypeModbusTcp,
   377  						Properties: nil,
   378  						Response:   b,
   379  						Status:     "",
   380  					}
   381  					b, _ = json.Marshal(resp)
   382  					_ = mqttCli.Publish("system/plugins/node/second/resp/plugin.test", b)
   383  					_ = mqttCli.Publish(fmt.Sprintf("system/plugins/node/second/resp/%s", plugEnt.Id), b)
   384  
   385  				})
   386  			})
   387  		})
   388  	})
   389  }