diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index f3083323..46f4ec36 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -4,7 +4,8 @@ "consoleFull" ], "ignore": [ - "**/__snapshots__/**" + "**/__snapshots__/**", + "**/mqtt_test/schemas/**" ], "absolute": true } \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e411ac0b..a57907f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,4 +153,14 @@ tags: | ghcr.io/${{ env.IMAGE_REPOSITORY }}:${{ env.IMAGE_TAG }} provenance: false + - name: Run integration test mqtt + if: ${{env.IS_NOT_PR == 'true'}} + run : | + #! /bin/bash + python -m pip install --upgrade pip + cd ./UmatiDashboardOpcUaClient/Tests/integration/mqtt_test || exit + pip install -r requirements.txt + docker compose up -d + ./waitForContainer.sh + python -m unittest discover test_mqtt_sampleserver \ No newline at end of file diff --git a/README.md b/README.md index 066115fd..bfaa8399 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Follow these instructions to use the client as a testing tool for your implement For the local instance testing you need to run your own MQTT Broker and a MQTT Client. See [MQTT Doc](doc/MQTT.md) for more information and instructions -[Here](example/SampleServer/) is an docker-compose example including a mqtt broker, a umati Sample Server and the gateway. The example contains also the need configuration for the samples. +[Here](example/ShowcaseDeployment/) is an docker-compose example including a mqtt broker, a umati Sample Server and the gateway. The example contains also the need configuration for the samples. ### Usage for connecting a server to the dashboard diff --git a/Tests/integration/mqtt_test/configuration.json b/Tests/integration/mqtt_test/configuration.json new file mode 100644 index 00000000..a0bc3f8f --- /dev/null +++ b/Tests/integration/mqtt_test/configuration.json @@ -0,0 +1,305 @@ +{ + "OpcUa": { + "Endpoint": "opc.tcp://opcuaserver:4840", + "Username": "", + "Password": "", + "Security": 1 + }, + "Mqtt": { + "Hostname": "mqtt_broker", + "Port": 1883, + "Username": "umati/mqtt_test", + "Password": "", + "Prefix": "umati/v2", + "ClientId": "umati/mqtt_test", + "Protocol": "tcp" + }, + "MachineCacheFile": "MachineCache_datahub.json", + "MachinesFilter": [ ], + "ObjectTypeNamespaces": [ + "http://opcfoundation.org/UA/", + "http://opcfoundation.org/UA/IA/", + "http://opcfoundation.org/UA/DI/", + "http://opcfoundation.org/UA/Dictionary/IRDI", + "http://opcfoundation.org/UA/Machinery/", + "http://opcfoundation.org/UA/Machinery/ProcessValues/", + "http://opcfoundation.org/UA/Machinery/Result/", + "http://opcfoundation.org/UA/MachineTool/", + "http://opcfoundation.org/UA/PADIM/", + "http://opcfoundation.org/UA/AdditiveManufacturing/NodeSet2/", + "http://opcfoundation.org/UA/Woodworking/", + "http://opcfoundation.org/UA/Robotics/", + "http://opcfoundation.org/UA/IJT/", + "http://opcfoundation.org/GMS/", + "http://opcfoundation.org/UA/PlasticsRubber/GeneralTypes/", + "http://opcfoundation.org/UA/PlasticsRubber/IMM2MES/", + "http://opcfoundation.org/UA/PlasticsRubber/TCD/", + "http://opcfoundation.org/UA/PlasticsRubber/HotRunner/", + "http://opcfoundation.org/UA/PlasticsRubber/LDS/", + "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/GeneralTypes/", + "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/HaulOff/", + "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/Corrugator/", + "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/Extruder/", + "http://opcfoundation.org/UA/PlasticsRubber/umati/generic/", + "http://opcfoundation.org/UA/PlasticsRubber/umati/OPC40079ForUmati/", + "http://opcfoundation.org/UA/PlasticsRubber/ImmToRobot/", + "http://opcfoundation.org/UA/Glass/Flat/", + "http://lenord.de/umati/MiniCODER/" + ], + "NamespaceInformations": [ + { + "Namespace": "http://opcfoundation.org/UA/Robotics/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/Robotics/", + "Id": "i=1004", + "BaseTypeLevel": 0, + "$comment": "MotionDeviceType" + }, + { + "Uri": "http://opcfoundation.org/UA/Robotics/", + "Id": "i=1003", + "BaseTypeLevel": 0, + "$comment": "ControllerType" + }, + { + "Uri": "http://opcfoundation.org/UA/Robotics/", + "Id": "i=1002", + "BaseTypeLevel": 0, + "$comment": "MotionDeviceSystemType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "BaseTypeLevel": 0, + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/IJT/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/IJT/", + "Id": "i=1005", + "$comment": "TighteningSystemType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/MachineTool-Prototyping/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/MachineTool-Prototyping/", + "Id": "i=1014", + "$comment": "MachineToolType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/MachineTool-Prototyping/", + "Id": "i=1012", + "$comment": "MachineToolIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/MachineTool/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/MachineTool/", + "Id": "i=13", + "$comment": "MachineToolType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/MachineTool/", + "Id": "i=11", + "$comment": "MachineToolIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/Woodworking/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/Woodworking/", + "Id": "i=2", + "$comment": "WwMachineType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/GMS/", + "Types": [ + { + "Uri": "http://opcfoundation.org/GMS/", + "Id": "i=1002", + "$comment": "GMSType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/GMS/", + "Id": "i=1011", + "$comment": "GMSIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/umati/generic/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/umati/generic/", + "Id": "i=1002", + "$comment": "UmatiPlasticsRubberGenericType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/IMM2MES/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/IMM2MES/", + "Id": "i=1007", + "$comment": "IMM_MES_InterfaceType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/Extruder/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/Extruder/", + "Id": "i=1015", + "$comment": "Extruder_InterfaceType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/Corrugator/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/Extrusion_v2/Corrugator/", + "Id": "i=1003", + "$comment": "Corrugator_InterfaceType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/LDS/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/LDS/", + "Id": "i=1007", + "$comment": "LDS_InterfaceType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/HotRunner/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/HotRunner/", + "Id": "i=1010", + "$comment": "HRD_InterfaceType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/TCD/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/TCD/", + "Id": "i=1012", + "$comment": "TCD_InterfaceType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/PlasticsRubber/umati/OPC40079ForUmati/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/PlasticsRubber/umati/OPC40079ForUmati/", + "Id": "i=1003", + "$comment": "ImmRobotCellType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Machinery/", + "Id": "i=1012", + "$comment": "MachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/Glass/Flat/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/Glass/Flat/", + "Id": "i=1015", + "$comment": "GlassMachineType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/Glass/Flat/", + "Id": "i=1020", + "$comment": "GlassMachineIdentificationType" + } + }, + { + "Namespace": "http://opcfoundation.org/UA/AdditiveManufacturing/NodeSet2/", + "Types": [ + { + "Uri": "http://opcfoundation.org/UA/AdditiveManufacturing/NodeSet2/", + "Id": "i=1031", + "$comment": "AdditiveManufacturingType" + } + ], + "IdentificationType": { + "Uri": "http://opcfoundation.org/UA/AdditiveManufacturing/NodeSet2/", + "Id": "i=1028", + "$comment": "MachineIdentificationAMType" + } + } + ] +} diff --git a/Tests/integration/mqtt_test/docker-compose.yml b/Tests/integration/mqtt_test/docker-compose.yml new file mode 100644 index 00000000..09195a74 --- /dev/null +++ b/Tests/integration/mqtt_test/docker-compose.yml @@ -0,0 +1,20 @@ +--- +version: '3.1' +services: + mqtt_broker: + image: eclipse-mosquitto + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf + ports: + - 1883:1883 + opcuaserver: + image: ghcr.io/umati/sample-server:develop + ports: + - 4840:4840 + gateway: + depends_on: + - mqtt_broker + - opcuaserver + image: ghcr.io/${IMAGE_REPOSITORY}:${IMAGE_TAG} + volumes: + - ./configuration.json:/app/configuration.json diff --git a/Tests/integration/mqtt_test/mosquitto.conf b/Tests/integration/mqtt_test/mosquitto.conf new file mode 100644 index 00000000..324b1915 --- /dev/null +++ b/Tests/integration/mqtt_test/mosquitto.conf @@ -0,0 +1,7 @@ +listener 1883 +protocol mqtt + +log_dest stdout +log_type all + +allow_anonymous true diff --git a/Tests/integration/mqtt_test/requirements.txt b/Tests/integration/mqtt_test/requirements.txt new file mode 100644 index 00000000..f52dd97b --- /dev/null +++ b/Tests/integration/mqtt_test/requirements.txt @@ -0,0 +1,3 @@ +jsonschema>=4.17.3 +paho-mqtt>=1.6.1 + diff --git a/Tests/integration/mqtt_test/schemas/SampleServer/BaseMachineTool.json b/Tests/integration/mqtt_test/schemas/SampleServer/BaseMachineTool.json new file mode 100644 index 00000000..07b242eb --- /dev/null +++ b/Tests/integration/mqtt_test/schemas/SampleServer/BaseMachineTool.json @@ -0,0 +1,372 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "Identification": { + "type": "object", + "properties": { + "Manufacturer": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "locale", + "text" + ] + }, + "ProductInstanceUri": { + "type": "string" + }, + "SerialNumber": { + "type": "string" + } + }, + "required": [ + "Manufacturer", + "ProductInstanceUri", + "SerialNumber" + ] + }, + "Monitoring": { + "type": "object", + "properties": { + "": { + "type": "object", + "properties": { + "Channel 1": { + "type": "object", + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "ChannelMode": { + "type": "integer" + }, + "ChannelState": { + "type": "integer" + }, + "FeedOverride": { + "type": "object", + "properties": { + "properties": { + "type": "object", + "properties": { + "EURange": { + "type": "object", + "properties": { + "High": { + "type": "number" + }, + "Low": { + "type": "number" + } + }, + "required": [ + "High", + "Low" + ] + }, + "EngineeringUnits": { + "type": "object", + "properties": { + "Description": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "locale", + "text" + ] + }, + "DisplayName": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "locale", + "text" + ] + }, + "NamespaceUri": { + "type": "string" + }, + "UnitId": { + "type": "integer" + } + }, + "required": [ + "Description", + "DisplayName", + "NamespaceUri", + "UnitId" + ] + } + }, + "required": [ + "EURange", + "EngineeringUnits" + ] + }, + "value": { + "type": "number" + } + }, + "required": [ + "properties", + "value" + ] + }, + "Name": { + "type": "string" + } + }, + "required": [ + "$TypeDefinition", + "ChannelMode", + "ChannelState", + "FeedOverride", + "Name" + ] + } + }, + "required": [ + "Channel 1" + ] + }, + "MachineTool": { + "type": "object", + "properties": { + "OperationMode": { + "type": "integer" + }, + "PowerOnDuration": { + "type": "integer" + } + }, + "required": [ + "OperationMode", + "PowerOnDuration" + ] + }, + "Stacklight": { + "type": "object", + "properties": { + "": { + "type": "object", + "properties": { + "Light 0": { + "type": "object", + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "IsPartOfBase": { + "type": "boolean" + }, + "NumberInList": { + "type": "integer" + }, + "SignalColor": { + "type": "integer" + }, + "SignalMode": { + "type": "integer" + }, + "SignalOn": { + "type": "boolean" + } + }, + "required": [ + "$TypeDefinition", + "IsPartOfBase", + "NumberInList", + "SignalColor", + "SignalMode", + "SignalOn" + ] + }, + "Light 1": { + "type": "object", + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "IsPartOfBase": { + "type": "boolean" + }, + "NumberInList": { + "type": "integer" + }, + "SignalColor": { + "type": "integer" + }, + "SignalMode": { + "type": "integer" + }, + "SignalOn": { + "type": "boolean" + } + }, + "required": [ + "$TypeDefinition", + "IsPartOfBase", + "NumberInList", + "SignalColor", + "SignalMode", + "SignalOn" + ] + }, + "Light 2": { + "type": "object", + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "IsPartOfBase": { + "type": "boolean" + }, + "NumberInList": { + "type": "integer" + }, + "SignalColor": { + "type": "integer" + }, + "SignalMode": { + "type": "integer" + }, + "SignalOn": { + "type": "boolean" + } + }, + "required": [ + "$TypeDefinition", + "IsPartOfBase", + "NumberInList", + "SignalColor", + "SignalMode", + "SignalOn" + ] + } + }, + "required": [ + "Light 0", + "Light 1", + "Light 2" + ] + }, + "NodeVersion": { + "type": "string" + }, + "StacklightMode": { + "type": "integer" + } + }, + "required": [ + "", + "NodeVersion", + "StacklightMode" + ] + } + }, + "required": [ + "", + "MachineTool", + "Stacklight" + ] + }, + "Production": { + "type": "object", + "properties": { + "ActiveProgram": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "NumberInList": { + "type": "integer" + }, + "State": { + "type": "object", + "properties": { + "CurrentState": { + "type": "object", + "properties": { + "properties": { + "type": "object", + "properties": { + "Id": { + "type": "integer" + }, + "Number": { + "type": "integer" + } + }, + "required": [ + "Id", + "Number" + ] + }, + "value": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "locale", + "text" + ] + } + }, + "required": [ + "properties", + "value" + ] + } + }, + "required": [ + "CurrentState" + ] + } + }, + "required": [ + "Name", + "NumberInList", + "State" + ] + } + }, + "required": [ + "ActiveProgram" + ] + } + }, + "required": [ + "Identification", + "Monitoring", + "Production" + ] +} \ No newline at end of file diff --git a/Tests/integration/mqtt_test/schemas/SampleServer/FullMachineTool.json b/Tests/integration/mqtt_test/schemas/SampleServer/FullMachineTool.json new file mode 100644 index 00000000..f86373b6 --- /dev/null +++ b/Tests/integration/mqtt_test/schemas/SampleServer/FullMachineTool.json @@ -0,0 +1,898 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/Welcome9", + "definitions": { + "Welcome9": { + "type": "object", + "additionalProperties": false, + "properties": { + "Equipment": { + "$ref": "#/definitions/Equipment" + }, + "Identification": { + "$ref": "#/definitions/Identification" + }, + "Monitoring": { + "$ref": "#/definitions/Monitoring" + }, + "Notification": { + "$ref": "#/definitions/Notification" + }, + "Production": { + "$ref": "#/definitions/Production" + } + }, + "required": [ + "Equipment", + "Identification", + "Monitoring", + "Notification", + "Production" + ], + "title": "Welcome9" + }, + "Equipment": { + "type": "object", + "additionalProperties": false, + "properties": { + "Tools": { + "$ref": "#/definitions/Tools" + } + }, + "required": [ + "Tools" + ], + "title": "Equipment" + }, + "Tools": { + "type": "object", + "additionalProperties": false, + "properties": { + "": { + "$ref": "#/definitions/ToolsTool" + }, + "NodeVersion": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "", + "NodeVersion" + ], + "title": "Tools" + }, + "ToolsTool": { + "type": "object", + "additionalProperties": false, + "properties": { + "Multi 1": { + "$ref": "#/definitions/Multi1" + }, + "Tool1": { + "$ref": "#/definitions/Tool1" + } + }, + "required": [ + "Multi 1", + "Tool1" + ], + "title": "ToolsTool" + }, + "Multi1": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "": { + "$ref": "#/definitions/Multi1_" + ], + "title": "Multi1" + }, + "Multi1_": { + "$ref": "#/definitions/MonitoredElement" + }, + "MachineTool": { + "$ref": "#/definitions/MachineTool" + }, + "Stacklight": { + "$ref": "#/definitions/Stacklight" + } + }, + "required": [ + "", + "MachineTool", + "Stacklight" + ], + "title": "Monitoring" + }, + "MonitoredElement": { + "type": "object", + "additionalProperties": false, + "properties": { + "Channel 1": { + "$ref": "#/definitions/Channel" + }, + "Channel 2": { + "$ref": "#/definitions/Channel" + }, + "Channel 3": { + "$ref": "#/definitions/Channel" + }, + "Channel 4": { + "$ref": "#/definitions/Channel" + }, + "EDM": { + "$ref": "#/definitions/Edm" + }, + "Laser": { + "$ref": "#/definitions/Laser" + }, + "Spindle 1": { + "$ref": "#/definitions/Spindle1" + } + }, + "required": [ + "Channel 1", + "Channel 2", + "Channel 3", + "Channel 4", + "EDM", + "Laser", + "Spindle 1" + ], + "title": "MonitoredElement" + }, + "Channel": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "ChannelMode": { + "type": "integer" + }, + "ChannelState": { + "type": "integer" + }, + "FeedOverride": { + "$ref": "#/definitions/Override" + }, + "Name": { + "type": "string" + } + }, + "required": [ + "$TypeDefinition", + "ChannelMode", + "ChannelState", + "FeedOverride", + "Name" + ], + "title": "Channel" + }, + "Override": { + "type": "object", + "additionalProperties": false, + "properties": { + "properties": { + "$ref": "#/definitions/FeedOverrideProperties" + }, + "value": { + "type": "number" + } + }, + "required": [ + "properties", + "value" + ], + "title": "Override" + }, + "FeedOverrideProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "EURange": { + "$ref": "#/definitions/EURange" + }, + "EngineeringUnits": { + "$ref": "#/definitions/EngineeringUnits" + } + }, + "required": [ + "EURange", + "EngineeringUnits" + ], + "title": "FeedOverrideProperties" + }, + "EURange": { + "type": "object", + "additionalProperties": false, + "properties": { + "High": { + "type": "number" + }, + "Low": { + "type": "number" + } + }, + "required": [ + "High", + "Low" + ], + "title": "EURange" + }, + "EngineeringUnits": { + "type": "object", + "additionalProperties": false, + "properties": { + "Description": { + "$ref": "#/definitions/ComponentName" + }, + "DisplayName": { + "$ref": "#/definitions/ComponentName" + }, + "NamespaceUri": { + "type": "string" + }, + "UnitId": { + "type": "integer" + } + }, + "required": [ + "Description", + "DisplayName", + "NamespaceUri", + "UnitId" + ], + "title": "EngineeringUnits" + }, + "Edm": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "EDMGeneratorState": { + "type": "integer" + }, + "IsOn": { + "type": "boolean" + }, + "Name": { + "type": "string" + } + }, + "required": [ + "$TypeDefinition", + "EDMGeneratorState", + "IsOn", + "Name" + ], + "title": "Edm" + }, + "Laser": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "ControllerIsOn": { + "type": "boolean" + }, + "LaserState": { + "type": "integer" + }, + "Name": { + "type": "string" + } + }, + "required": [ + "$TypeDefinition", + "ControllerIsOn", + "LaserState", + "Name" + ], + "title": "Laser" + }, + "Spindle1": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "IsRotating": { + "type": "boolean" + }, + "IsUsedAsAxis": { + "type": "boolean" + }, + "Name": { + "type": "string" + }, + "Override": { + "$ref": "#/definitions/Override" + } + }, + "required": [ + "$TypeDefinition", + "IsRotating", + "IsUsedAsAxis", + "Name", + "Override" + ], + "title": "Spindle1" + }, + "MachineTool": { + "type": "object", + "additionalProperties": false, + "properties": { + "OperationMode": { + "type": "integer" + }, + "PowerOnDuration": { + "type": "integer" + } + }, + "required": [ + "OperationMode", + "PowerOnDuration" + ], + "title": "MachineTool" + }, + "Stacklight": { + "type": "object", + "additionalProperties": false, + "properties": { + "": { + "$ref": "#/definitions/StacklightOrderedObject" + }, + "NodeVersion": { + "type": "string" + }, + "StacklightMode": { + "type": "integer" + } + }, + "required": [ + "", + "NodeVersion", + "StacklightMode" + ], + "title": "Stacklight" + }, + "StacklightOrderedObject": { + "type": "object", + "additionalProperties": false, + "properties": { + "Light 0": { + "$ref": "#/definitions/Light" + }, + "Light 1": { + "$ref": "#/definitions/Light" + }, + "Light 2": { + "$ref": "#/definitions/Light" + }, + "Light 3": { + "$ref": "#/definitions/Light" + } + }, + "required": [ + "Light 0", + "Light 1", + "Light 2", + "Light 3" + ], + "title": "StacklightOrderedObject" + }, + "Light": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "IsPartOfBase": { + "type": "boolean" + }, + "NumberInList": { + "type": "integer" + }, + "SignalColor": { + "type": "integer" + }, + "SignalMode": { + "type": "integer" + }, + "SignalOn": { + "type": "boolean" + } + }, + "required": [ + "$TypeDefinition", + "IsPartOfBase", + "NumberInList", + "SignalColor", + "SignalMode", + "SignalOn" + ], + "title": "Light" + }, + "Notification": { + "type": "object", + "additionalProperties": false, + "properties": { + "Prognoses": { + "$ref": "#/definitions/Prognoses" + } + }, + "required": [ + "Prognoses" + ], + "title": "Notification" + }, + "Prognoses": { + "type": "object", + "additionalProperties": false, + "properties": { + "": { + "$ref": "#/definitions/Prognosis" + }, + "NodeVersion": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "", + "NodeVersion" + ], + "title": "Prognoses" + }, + "Prognosis": { + "type": "object", + "additionalProperties": false, + "properties": { + "Maintenance": { + "$ref": "#/definitions/Maintenance" + }, + "Manual": { + "$ref": "#/definitions/Maintenance" + }, + "PartLoad": { + "$ref": "#/definitions/Load" + }, + "PartUnLoad": { + "$ref": "#/definitions/Load" + }, + "ProcessChangeover": { + "$ref": "#/definitions/Maintenance" + }, + "ProductionJobEnd": { + "$ref": "#/definitions/ProductionJobEnd" + }, + "ToolChange": { + "$ref": "#/definitions/Maintenance" + }, + "ToolLoad": { + "$ref": "#/definitions/Load" + }, + "ToolUnLoad": { + "$ref": "#/definitions/Load" + }, + "UtilityChange": { + "$ref": "#/definitions/UtilityChange" + } + }, + "required": [ + "Maintenance", + "Manual", + "PartLoad", + "PartUnLoad", + "ProcessChangeover", + "ProductionJobEnd", + "ToolChange", + "ToolLoad", + "ToolUnLoad", + "UtilityChange" + ], + "title": "Prognosis" + }, + "Maintenance": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "Activity": { + "$ref": "#/definitions/ComponentName" + }, + "PredictedTime": { + "type": "string" + }, + "Location": { + "$ref": "#/definitions/ComponentName" + } + }, + "required": [ + "$TypeDefinition", + "PredictedTime" + ], + "title": "Maintenance" + }, + "Load": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "Location": { + "$ref": "#/definitions/ComponentName" + }, + "PartName": { + "type": "string" + }, + "PredictedTime": { + "type": "string" + } + }, + "required": [ + "$TypeDefinition", + "Location", + "PredictedTime" + ], + "title": "Load" + }, + "ProductionJobEnd": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "PredictedTime": { + "type": "string" + }, + "SourceIdentifier": { + "type": "string" + } + }, + "required": [ + "$TypeDefinition", + "PredictedTime", + "SourceIdentifier" + ], + "title": "ProductionJobEnd" + }, + "UtilityChange": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "PredictedTime": { + "type": "string" + }, + "UtilityName": { + "type": "string" + } + }, + "required": [ + "$TypeDefinition", + "PredictedTime", + "UtilityName" + ], + "title": "UtilityChange" + }, + "Production": { + "type": "object", + "additionalProperties": false, + "properties": { + "ActiveProgram": { + "$ref": "#/definitions/ActiveProgram" + }, + "ProductionPlan": { + "$ref": "#/definitions/ProductionPlan" + } + }, + "required": [ + "ActiveProgram", + "ProductionPlan" + ], + "title": "Production" + }, + "ActiveProgram": { + "type": "object", + "additionalProperties": false, + "properties": { + "JobIdentifier": { + "type": "string" + }, + "JobNodeId": { + "type": "integer" + }, + "Name": { + "type": "string" + }, + "NumberInList": { + "type": "integer" + }, + "State": { + "$ref": "#/definitions/State" + } + }, + "required": [ + "JobIdentifier", + "JobNodeId", + "Name", + "NumberInList", + "State" + ], + "title": "ActiveProgram" + }, + "State": { + "type": "object", + "additionalProperties": false, + "properties": { + "CurrentState": { + "$ref": "#/definitions/CurrentState" + } + }, + "required": [ + "CurrentState" + ], + "title": "State" + }, + "CurrentState": { + "type": "object", + "additionalProperties": false, + "properties": { + "properties": { + "$ref": "#/definitions/CurrentStateProperties" + }, + "value": { + "$ref": "#/definitions/ComponentName" + } + }, + "required": [ + "properties", + "value" + ], + "title": "CurrentState" + }, + "CurrentStateProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "Id": { + "type": "integer" + }, + "Number": { + "type": "integer" + } + }, + "required": [ + "Id", + "Number" + ], + "title": "CurrentStateProperties" + }, + "ProductionPlan": { + "type": "object", + "additionalProperties": false, + "properties": { + "": { + "$ref": "#/definitions/ProductionPlanOrderedObject" + }, + "NodeVersion": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "", + "NodeVersion" + ], + "title": "ProductionPlan" + }, + "ProductionPlanOrderedObject": { + "type": "object", + "additionalProperties": false, + "properties": { + "MyJob 1": { + "$ref": "#/definitions/MyJob1" + } + }, + "required": [ + "MyJob 1" + ], + "title": "ProductionPlanOrderedObject" + }, + "MyJob1": { + "type": "object", + "additionalProperties": false, + "properties": { + "$TypeDefinition": { + "type": "string" + }, + "Identifier": { + "type": "string" + }, + "NumberInList": { + "type": "integer" + }, + "RunsCompleted": { + "type": "integer" + }, + "RunsPlanned": { + "type": "integer" + }, + "State": { + "$ref": "#/definitions/State" + } + }, + "required": [ + "$TypeDefinition", + "Identifier", + "NumberInList", + "RunsCompleted", + "RunsPlanned", + "State" + ], + "title": "MyJob1" + }, + "Locale": { + "type": "string", + "enum": [ + "", + "en" + ], + "title": "Locale" + } + } +} diff --git a/Tests/integration/mqtt_test/test_mqtt_sampleserver.py b/Tests/integration/mqtt_test/test_mqtt_sampleserver.py new file mode 100644 index 00000000..f86b4041 --- /dev/null +++ b/Tests/integration/mqtt_test/test_mqtt_sampleserver.py @@ -0,0 +1,137 @@ +""" +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Copyright 2023 (c) Sebastian Friedl, FVA GmbH / interop4X(for umati and VDW e.V.) +""" + +import json +import time +import unittest +from typing import Any, Optional + +import jsonschema +import paho.mqtt.client as mqtt + + +class TestMqttSampleServer(unittest.TestCase): + client = mqtt.Client() + + @classmethod + def setUpClass(cls) -> None: + """ + A class method called before tests in an individual class are run. + Here we establish the MQTT client connection. + """ + ret = None + for i in range(0, 120): + try: + ret = cls.client.connect("localhost", 1883, 60) + except ConnectionRefusedError: + print(f"Try to connect to mqtt broker {i} times") + time.sleep(1) + assert ret == 0 + + @classmethod + def tearDownClass(cls) -> None: + """ + A class method called after all tests in an individual class have run. + Here we disconnect the MQTT client connection. + """ + ret = cls.client.disconnect() + assert ret == 0 + + def test_clientOnline_status(self): + """Tests if the client online status message is as expected.""" + received_msg = self.receive_message("umati/v2/umati/mqtt_test/clientOnline") + self.assertEqual(received_msg, b"1") + + def test_BaseMachineTool(self) -> None: + """ + This test function test if the BaseMachineTool is send correct to the mqtt broker + """ + # Use the helper method to receive the message as JSON + topic = "umati/v2/umati/mqtt_test/MachineToolType/nsu=http:_2F_2Fexample.com_2FBasicMachineTool_2F;i=66382" + json_msg = self.receive_message_as_json(topic) + + # Load the JSON schema from a file. + with open("schemas/SampleServer/BaseMachineTool.json", "r") as f: + schema = json.load(f) + + # Validate the received message against the JSON schema. + # In case of validation errors, the jsonschema.validate() function will raise an exception. + try: + jsonschema.validate(instance=json_msg, schema=schema) + except jsonschema.exceptions.ValidationError as e: + print(e) + self.fail(f"Message is of {topic} is not correct!") + + def test_FullMachineTool(self) -> None: + """ + This test function test if the FullMachineTool is send correct to the mqtt broker + """ + # Use the helper method to receive the message as JSON + topic = "umati/v2/umati/mqtt_test/MachineToolType/nsu=http:_2F_2Fexample.com_2FFullMachineTool_2F;i=66382" + json_msg = self.receive_message_as_json(topic) + + # Load the JSON schema from a file. + # you can use https://codebeautify.org/json-to-json-schema-generator to generate a schema from a example json + with open("schemas/SampleServer/FullMachineTool.json", "r") as f: + schema = json.load(f) + + # Validate the received message against the JSON schema. + # In case of validation errors, the jsonschema.validate() function will raise an exception. + try: + jsonschema.validate(instance=json_msg, schema=schema) + except jsonschema.exceptions.ValidationError as e: + print(e) + self.fail(f"Message is of {topic} is not correct!") + + def receive_message(self, topic: str, timeout: int = 10) -> Any: + """ + This helper function subscribes to a topic and waits for a message or until the timeout. + It then returns the received message as bytes. + + Parameters: + - topic: the MQTT topic to subscribe to. + - timeout: the amount of time in seconds to wait for a message. Default is 10 seconds. + + Returns: + - The payload of the received MQTT message as bytes. + """ + received_msg = None + + def on_message(client, userdata, msg): + nonlocal received_msg + if msg.topic == topic: + received_msg = msg.payload + + self.client.on_message = on_message + self.client.subscribe(topic) + timeout_time = time.time() + timeout + while received_msg is None and time.time() < timeout_time: + self.client.loop() + return received_msg + + def receive_message_as_json(self, topic: str, timeout: int = 10) -> dict: + """ + This helper function uses receive_message() to get a message, + then it decodes and deserializes it from JSON to a Python dictionary and returns it. + + Parameters: + - topic: the MQTT topic to subscribe to. + - timeout: the amount of time in seconds to wait for a message. Default is 10 seconds. + + Returns: + - The payload of the received MQTT message as a Python dictionary. + """ + received_msg: Optional[bytes] = self.receive_message(topic, timeout) + if received_msg is not None: + return json.loads(received_msg.decode("utf-8")) + else: + return {} + + +if __name__ == "__main__": + unittest.main() diff --git a/Tests/integration/mqtt_test/waitForContainer.sh b/Tests/integration/mqtt_test/waitForContainer.sh new file mode 100755 index 00000000..56d7811b --- /dev/null +++ b/Tests/integration/mqtt_test/waitForContainer.sh @@ -0,0 +1,10 @@ +#!/bin/bash +NEXT_WAITTIME=0 +WAITTIME_LIMIT_SEC=600 +while [[ "$(docker logs mqtt_test-gateway-1 | grep -c "nsu=http:_2F_2Fexample.com_2FFullMachineTool_2F")" != "2" && "$NEXT_WAITTIME" != "$WAITTIME_LIMIT_SEC" ]] +do + echo "Waiting for test container to become ready since ${NEXT_WAITTIME}s..." + sleep 5 + NEXT_WAITTIME=$((NEXT_WAITTIME + 5)) +done +sleep 5 \ No newline at end of file