Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[98] remove dependency to oee-bundle #99

Merged
merged 21 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
release_version: ${{ steps.update-version-number.outputs.release_version }}
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: 'repo'

Expand All @@ -59,9 +59,9 @@ jobs:
docker rmi oee-simulators

- name: Upload file and zip an artifact
uses: actions/upload-artifact@v3.1.2
uses: actions/upload-artifact@v4
with:
name: 'oee-simulators'
path: |
repo/simulators/image.tar
repo/simulators/cumulocity.json
repo/simulators/cumulocity.json
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
path: 'repo'

- name: Download artifact
uses: actions/download-artifact@v3.0.2
uses: actions/download-artifact@v4
with:
name: 'oee-simulators'
path: 'repo/simulators'
Expand Down Expand Up @@ -74,4 +74,4 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }} # upload_url output is provided automatically by the GitHub API when the release is created
asset_path: 'repo/simulators/oee-simulators.zip'
asset_name: 'oee-simulators.zip'
asset_content_type: application/zip
asset_content_type: application/zip
60 changes: 0 additions & 60 deletions simulators/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,66 +9,6 @@
"envFile": "${workspaceFolder}/.vscode/.env",
"cwd": "${workspaceFolder}/main/",
"console": "integratedTerminal"
},
{
"name": "create profiles",
"type": "python",
"request": "launch",
"program": "profile_generator.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.vscode/.env",
"cwd": "${workspaceFolder}/main/",
"args": [
"--create-profiles"
]
},
{
"name": "remove simulator profiles via OEE API",
"type": "python",
"request": "launch",
"program": "profile_generator.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.vscode/.env",
"cwd": "${workspaceFolder}/main/",
"args": [
"--remove-simulator-profiles-via-oee"
]
},
{
"name": "delete simulator profiles",
"type": "python",
"request": "launch",
"program": "profile_generator.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.vscode/.env",
"cwd": "${workspaceFolder}/main/",
"args": [
"--delete-simulator-profiles"
]
},
{
"name": "create calculation categories",
"type": "python",
"request": "launch",
"program": "profile_generator.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.vscode/.env",
"cwd": "${workspaceFolder}/main/",
"args": [
"--create-categories"
]
},
{
"name": "delete calculation categories",
"type": "python",
"request": "launch",
"program": "profile_generator.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.vscode/.env",
"cwd": "${workspaceFolder}/main/",
"args": [
"--delete-categories"
]
}
]
}
3 changes: 0 additions & 3 deletions simulators/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,4 @@ WORKDIR /home/appuser

USER appuser

ARG ARG_CREATE_PROFILES
ENV CREATE_PROFILES ${ARG_CREATE_PROFILES:-false}

CMD [ "python", "./simulator.py" ]
100 changes: 2 additions & 98 deletions simulators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

OEE-simulators offers these main features in [main](main):
- the **oee-simulators microservice** which creates devices and sends data into Cumulocity.
- the **profile generator** to create/delete OEE calculation profiles from the command line.

There are extra features in [extras](extras):
- the **export profile data** to export Measurements or/and Alarms from OEE profiles into json data files.
Expand All @@ -15,7 +14,6 @@ Detailed feature list:
- identifies devices using a configurable `externalId`.
- devices can be disabled to not send any events and measurements.
- Written in python which can be modified easily for further development.
- Simulators work based on status and shift that it is assigned.

### Simulator definition
Creating simulators in Cumulocity based on the definitions in [simulators.json](main/simulator.json). Those simulators can be used for profiles in the OEE App. The currently supported simulators and the corresponding profiles are described [here](simulators.md).
Expand Down Expand Up @@ -112,50 +110,11 @@ Example for a simulator definition:

- In measurements, `valueDistribution` is defined to let the simulator know which distribution formula to use to generate measurements. There are three choices that can be defined here: `uniform`, `uniformint`, `normal`.

- Simulates shutdowns (no events or measurements are sent if simulator is DOWN or out of shift)
- Simulates shutdowns (no events or measurements are sent if simulator is DOWN)

- the main entry point is [simulator.py](main/simulator.py)
- the script reads the configuration from [simulator.json](main/simulator.json) and creates a new device for every entry
- the `id` property is used as `external_id` for the ManagedObjects to avoid creating multiple devices when redeploying/updating the microservice

- Simulators act according to given Shiftplans
- Simulators are linked to shiftplans via locationId.
- The `locationId` presented in [shiftplans.json](main/shiftplans.json) are used to parse for all shiftplans used in the script but only a specific shiftplan is applied to a simulator if it has the `locationId` of a shift defined.
```
{
"type": "Simulator",
"id": "sim_012",
"label": "Normal #12 with short shifts",
"locationId": "ShortShiftsLocation",
"enabled": true
}
```
In this example, the ShortShiftsLocation shift is applied to the simulator sim_012.
- If a Simulator is not in Production time according to the given Shiftplan, it will not produce any events and measurements.
- At startup [shiftplans.json](main/shiftplans.json) is parsed once and the shifts are created accordingly (if they do not exist).
- Everytime an event/measurement is about to be sent, the script checks if the status of the locationId equals PRODUCTION; and only if the status is correct, the event/measurement is sent.
- Since the status of shiftplans is checked in realtime, if there are any changes to the shiftplans, they are taken into account.


### Shiftplan definition
```
[
{
"locationId": "OneShiftLocation",
"recurringTimeslots": [
{ "id": "OneShiftLocation-DayShift", "seriesPostfix": "DayShift", "slotType": "PRODUCTION", "slotStart": "2022-07-13T08:00:00Z", "slotEnd": "2022-07-13T16:00:00Z", "description": "Day Shift", "active": true, "slotRecurrence": { "weekdays": [1, 2, 3, 4, 5] } },
{ "id": "OneShiftLocation-Break", "slotType": "BREAK", "slotStart": "2022-07-13T12:00:00Z", "slotEnd": "2022-07-13T12:30:00Z", "description": "Day Shift Break", "active": true, "slotRecurrence": { "weekdays": [1, 2, 3, 4, 5] } }
]
},
{...}
]
```
This is an example of a shiftplan which contains a shift name `OneShiftLocation` in `locationId` field.
The `recurringTimeslots` field is an array which define components of the shift. In this case, this shift has two parts:
- `OneShiftLocation-DayShift` is the shift for `PRODUCTION`, in which the applied simulators will generate events and measurements. The length of the shift is defined by the abstraction between `slotEnd` and `slotStart` and the `active` field is set to true means that this component of the shift is activated. The field `slotRecurrence` defined which day of the week can the applied simulators work. `""weekdays": [1, 2, 3, 4, 5]` means the applied simulators will work from Monday to Friday.
- `OneShiftLocation-Break` is the shift for `BREAK`, in which the applied simulators will not generate events and measurements. The other setup is the same as above.

**Only `recurringTimeslots` is supported**, `timeslots` is not support.

### Build the docker image

Expand All @@ -173,30 +132,13 @@ zip oee-simulators.zip image.tar cumulocity.json
```
This zip file can then be uploaded as a Microservice to Cumulocity IoT.

### Creating profiles automatically

The creation of profiles can be configured using the Tenant Options on the given Cumulocity tenant. See the [Cumulocity REST API](https://cumulocity.com/api/10.14.0/#tag/Options) documentation for details. The Tenant Options must use the category: "simulators"

Some Options can be configured:
- CREATE_PROFILES - holds a string with 'CREATE' or 'CREATE_IF_NOT_EXISTS' to indicate if the profiles should be overwritten or only created if they not already exist. Default: 'CREATE_IF_NOT_EXISTS'
- CREATE_PROFILES_ARGUMENTS - can be used to set a string that is passed down as arguments to the profile creation script [Execution from command line / CLI arguments](#execution-from-command-line).
- CREATE_ASSET_HIERARCHY - holds a boolean with 'true' or 'false' to indicate whether the Simulator should create the asset hierarchy for OEE. If set to true it will create one SITE with one LINE that holds all devices and their profiles. Default: 'false'
- LOG_LEVEL - sets the used logging-level of the simulator. Choose between INFO and DEBUG. DEBUG does give a more detailed output of the Simulator's configuration and decisions. Default: 'INFO'
- DELETE_PROFILES - Holds a boolean either 'True' or 'False' to indicate whether OEE_Profiles created by the simulator should be deleted beforehand.

Note: A profile will be created and activated only if no other profiles are already defined for the particular device.

### Deployment

To deploy this project, upload the zip file to the Cumulocity as Microservice. The zip file can be created locally as described above or downloaded from the [releases](https://github.com/SoftwareAG/oee-simulators/releases) section.

## Profile generator

The Profile Generator is a [Python script](main/profile_generator.py) that creates OEE calculation profiles for each simulator available in the tenant. Every simulator needs a template with appropriate name (<external_id>_profile.template) in your local [main](main/profile_templates) folder. The simulators must have been created beforehand by deploying the [oee-simulators](#simulator-microservice) microservice.

### Environment

Install python 3.8.3+ on your system. Probably you'll need install some packages using *pip* commmand, e.g.
Install python 3.8.3+ on your system. Probably you'll need install some packages using *pip* command, e.g.
```
pip install requests
```
Expand All @@ -213,44 +155,6 @@ C8Y_PASSWORD=yourpassword
Additionally the following optional/debug variables can be set:
```
MOCK_C8Y_REQUESTS=false
PROFILES_PER_DEVICE=1
```

- if MOCK_C8Y_REQUESTS is set to true, no requests to the C8Y tenant are executed, but you can see what would have been executed in the log
- if PROFILES_PER_DEVICE is increased, more than one profile is created for each simulator; this might be useful when doing performance/scalability tests

### Execution

There are two ways to execute the profile generation. You can run it from the development environment [Visual Studio Code](#visual-studio-code) or from the command line.

#### Execution from command line
To execute the scripts from command line, open a command prompt and the *oee-simulators\simulators* folder.

To create profiles, execute:
```
python .\main\profile_generator.py -c
```

To remove profiles using the OEE API, execute:
```
python .\main\profile_generator.py -r
```

To remove profiles using the standard Cumulocity IoT API (e.g. if OEE is not installed on the tenant), execute:
```
python .\main\profile_generator.py -d
```

#### Execution in Visual Studio Code

- Open *simulators* folder in the VSC
- Install python plugin: ms-python.python
- adjust environemnt varibales in [.vscode/.env](.vscode/.env)
- click at the big run/debug icon in the left toolbar
- select the configuration that you want to use from the dropdown: `create profiles`, `remove simulator profiles via OEE API`, `delete simulator profiles`
- click the small green run/debug icon left of the dropdown in the top area to execute the configuration





4 changes: 2 additions & 2 deletions simulators/extras/ArgumentsAndCredentialsHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def SetupLogger(console_logger_name, console_log_level):


def HandleExportArguments():
parser = argparse.ArgumentParser(description='Script to export or import profiles data')
parser = argparse.ArgumentParser(description='Script to export or import data')
export_arg = parser.add_argument_group('Import')
export_arg.add_argument('--device-ids', '-i', type=str, help='Input device id / List of device ids', nargs='+')
export_arg.add_argument('--create-from', '-from', type=str, help='Input "create from" milestone')
Expand Down Expand Up @@ -118,7 +118,7 @@ def HandleExportArguments():


def HandleImportArguments():
parser = argparse.ArgumentParser(description='Script to import profiles data')
parser = argparse.ArgumentParser(description='Script to import data')

import_arg = parser.add_argument_group('Import')
import_arg.add_argument('--ifiles', '-i', type=str, help='Input file', nargs='+')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@

# Global variables and constants
logTimeFormat = "%Y%m%d%H%M%S_%f"
C8Y_PROFILE_GROUP = 'c8y_EventBasedSimulatorProfile'
C8Y_OEE_SIMULATOR_DEVICES_GROUP = "c8y_EventBasedSimulator"
DATA_TYPE, DEVICE_ID_LIST, CREATE_FROM, CREATE_TO, LOG_LEVEL, c8y, PASSWORD, TEST_FLAG = ArgumentsAndCredentialsHandler.HandleExportArguments()
C8Y_HEADERS, MEASUREMENTS_HEADERS = ArgumentsAndCredentialsHandler.SetupHeadersForAPIRequest(tenant_id=c8y.tenant_id, username= c8y.username, password=PASSWORD)
####################################################
# Setup Log
file_log_level = logging.DEBUG
console_log_level = LOG_LEVEL
consoleLogger = ArgumentsAndCredentialsHandler.SetupLogger(console_logger_name='ConsoleExportProfileData', console_log_level=console_log_level)
consoleLogger = ArgumentsAndCredentialsHandler.SetupLogger(console_logger_name='ConsoleExportData', console_log_level=console_log_level)
#####################################################

session = requests.Session()
Expand All @@ -32,7 +31,7 @@
######################################################


def ExportAllProfileDataFromChildDevices(createFrom, createTo):
def ExportAllDataFromChildDevices(createFrom, createTo):
deviceInTenantCount = 0
childDeviceCount = 0
deviceManagedObject = c8y.device_inventory.select(type=C8Y_OEE_SIMULATOR_DEVICES_GROUP)
Expand All @@ -43,21 +42,21 @@ def ExportAllProfileDataFromChildDevices(createFrom, createTo):
childDeviceCount += len(device.child_devices)
consoleLogger.info(f"List {len(device.child_devices)} of {device.name}'s child devices: ")
for childDevice in device.child_devices:
ExportSpecificProfileDataWithDeviceId(createFrom=createFrom,createTo=createTo, deviceId=childDevice.id)
ExportSpecificDataWithDeviceId(createFrom=createFrom, createTo=createTo, deviceId=childDevice.id)

if deviceInTenantCount == 0:
consoleLogger.info(f"No device in tenant {c8y.tenant_id} found")
else:
consoleLogger.info(f"There are {deviceInTenantCount} devices with {childDeviceCount} child devices in total")


def ExportSpecificProfileDataWithDeviceId(createFrom, createTo, deviceId):
def ExportSpecificDataWithDeviceId(createFrom, createTo, deviceId):
deviceName = FindDeviceNameById(deviceId, c8y.base_url)
consoleLogger.info(f"Child device {deviceName}, id #{deviceId}")
deviceExternalId, deviceExternalIdType = CheckDeviceExternalIdById(deviceId, c8y.base_url)
if not deviceExternalId:
return
if IsExternalIdTypeEventBasedSimulatorProfile(deviceExternalIdType):
if IsExternalIdTypeEventBasedSimulator(deviceExternalIdType):
filePath = CreateFilePath(Id=deviceExternalId)
else:
return
Expand Down Expand Up @@ -123,6 +122,7 @@ def ListMeasurements(deviceId, createFrom, createTo):


def AppendDataToJsonFile(jsonDataList, filePath, data_type, json_data={}):
consoleLogger.info(f"Adding {data_type} to file: {filePath}")
Dismissed Show dismissed Hide dismissed
# Create new json file or add data to an existing json file
with open(filePath, 'w') as f:
json_data[f"{data_type}"] = jsonDataList
Expand All @@ -141,7 +141,7 @@ def GetExternalIdReponse(deviceId, baseUrl):

def CheckDeviceExternalIdById(deviceId, baseUrl):
externalIdResponse = GetExternalIdReponse(deviceId, baseUrl)

consoleLogger.info(f"externalIdResponse for {deviceId}: {externalIdResponse.json()}")
Dismissed Show dismissed Hide dismissed
try:
deviceExternalId = externalIdResponse.json()['externalIds'][0]['externalId']
deviceExternalIdType = externalIdResponse.json()['externalIds'][0]['type']
Expand All @@ -153,11 +153,11 @@ def CheckDeviceExternalIdById(deviceId, baseUrl):
return deviceExternalId, deviceExternalIdType


def IsExternalIdTypeEventBasedSimulatorProfile(deviceExternalIdType):
if deviceExternalIdType == C8Y_PROFILE_GROUP:
def IsExternalIdTypeEventBasedSimulator(deviceExternalIdType):
if deviceExternalIdType == C8Y_OEE_SIMULATOR_DEVICES_GROUP:
return True
else:
consoleLogger.info(f"The type {deviceExternalIdType} of external ID must match with type {C8Y_PROFILE_GROUP}")
consoleLogger.info(f"The type {deviceExternalIdType} of the external ID is _not_ the same as type {C8Y_OEE_SIMULATOR_DEVICES_GROUP}!")
Dismissed Show dismissed Hide dismissed
return False


Expand All @@ -167,7 +167,7 @@ def CreateFilePath(Id):
os.makedirs('export_data')
relativeFilePath = f'export_data/{Id}.json'
filePath = os.path.join(os.getcwd(), relativeFilePath)
consoleLogger.debug(f"Created successfully file path: {filePath}")
consoleLogger.info(f"Created successfully file path: {filePath}")
Dismissed Show dismissed Hide dismissed
return filePath


Expand Down Expand Up @@ -213,7 +213,7 @@ def SetTimePeriodToExportData():
consoleLogger.info(f"and created before/to: {createTo}")

if not DEVICE_ID_LIST:
ExportAllProfileDataFromChildDevices(createFrom=createFrom, createTo=createTo)
ExportAllDataFromChildDevices(createFrom=createFrom, createTo=createTo)
else:
for deviceId in DEVICE_ID_LIST:
ExportSpecificProfileDataWithDeviceId(createFrom=createFrom, createTo=createTo, deviceId=deviceId)
ExportSpecificDataWithDeviceId(createFrom=createFrom, createTo=createTo, deviceId=deviceId)
Loading