Skip to content

Commit 4325a07

Browse files
authored
Merge pull request #18 from scfx/main
Availability event can be used with different status, durations and probabilites
2 parents 0e464cd + 2c4bc5f commit 4325a07

File tree

7 files changed

+281
-28
lines changed

7 files changed

+281
-28
lines changed

event-based-simulators/.vscode/launch.json

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
2-
// Use IntelliSense to learn about possible attributes.
3-
// Hover to view descriptions of existing attributes.
4-
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
52
"version": "0.2.0",
63
"configurations": [
74
{
@@ -48,6 +45,30 @@
4845
"args": [
4946
"--delete-simulator-profiles"
5047
]
48+
},
49+
{
50+
"name": "create calculation categories",
51+
"type": "python",
52+
"request": "launch",
53+
"program": "profile_generator.py",
54+
"console": "integratedTerminal",
55+
"envFile": "${workspaceFolder}/.vscode/.env",
56+
"cwd": "${workspaceFolder}/main/",
57+
"args": [
58+
"--create-categories"
59+
]
60+
},
61+
{
62+
"name": "delete calculation categories",
63+
"type": "python",
64+
"request": "launch",
65+
"program": "profile_generator.py",
66+
"console": "integratedTerminal",
67+
"envFile": "${workspaceFolder}/.vscode/.env",
68+
"cwd": "${workspaceFolder}/main/",
69+
"args": [
70+
"--delete-categories"
71+
]
5172
}
5273
]
5374
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"type": "OEECategoryConfiguration",
3+
"categories": [
4+
{
5+
"categoryInput": "Availability Loss (Time)",
6+
"name": "Planned maintenance",
7+
"id": "1",
8+
"status": "ACTIVE"
9+
},
10+
{
11+
"categoryInput": "Availability Loss (Time)",
12+
"name": "Manual stops",
13+
"id": "2",
14+
"status": "ACTIVE"
15+
}
16+
]
17+
}

event-based-simulators/main/cumulocityAPI.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class CumulocityAPI:
3131

3232
C8Y_SIMULATORS_GROUP = "c8y_EventBasedSimulator"
3333
OEE_CALCULATION_PROFILE_TYPE = "OEECalculationProfile"
34+
OEE_CALUCLATION_CATEGORY = "OEECategoryConfiguration"
3435

3536
def __init__(self) -> None:
3637
self.mocking = MOCK_REQUESTS.lower() == 'true'
@@ -59,13 +60,17 @@ def get_or_create_device(self, sim_id, label):
5960

6061
# Check if device already created
6162
return self.get_device_by_external_id(sim_id) or self.__create_device(sim_id, label)
62-
6363
def count_all_profiles(self):
64+
return self.__count_all(self.OEE_CALCULATION_PROFILE_TYPE)
65+
66+
def count_all_categories(self):
67+
return self.__count_all(self.OEE_CALUCLATION_CATEGORY)
68+
def __count_all(self, oee_type):
6469
if self.mocking:
65-
log.info(f'mock: count_profiles()')
70+
log.info(f'mock: count_all types({oee_type})')
6671
return 5
6772

68-
request_query = f'{C8Y_BASE}/inventory/managedObjects/count?type={self.OEE_CALCULATION_PROFILE_TYPE}'
73+
request_query = f'{C8Y_BASE}/inventory/managedObjects/count?type={oee_type}'
6974
repsonse = requests.get(request_query, headers=C8Y_HEADERS)
7075
if repsonse.ok:
7176
return repsonse.json()
@@ -109,6 +114,16 @@ def get_managed_object(self, id: str):
109114
self.log_warning_on_bad_repsonse(response)
110115
#TODO: check for errors
111116
return {}
117+
118+
def get_calculation_categories(self):
119+
if self.mocking:
120+
log.info(f'mock: get_managed_object()')
121+
return [{'id': '0'}]
122+
response = requests.get(C8Y_BASE + f'/inventory/managedObjects?type={self.OEE_CALUCLATION_CATEGORY}', headers=C8Y_HEADERS)
123+
if response.ok:
124+
return response.json()['managedObjects']
125+
self.log_warning_on_bad_repsonse(response)
126+
return {}
112127

113128
def delete_managed_object(self, id: str):
114129
if self.mocking:

event-based-simulators/main/event_based_simulators.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import time, json, os, logging, requests, base64
22
from datetime import datetime
3-
from random import randint, uniform
3+
from random import randint, uniform, choices
44

55
from cumulocityAPI import C8Y_BASE, C8Y_TENANT, C8Y_USER, C8Y_PASSWORD, CumulocityAPI
66
from oeeAPI import OeeAPI, ProfileCreateMode
@@ -34,6 +34,16 @@ def try_event(probability: float):
3434
'''
3535
return uniform(0.0, 1.0) <= probability
3636

37+
def get_random_status(statusses, durations, probabilites):
38+
'''returns a random status and duration of the given lists of status, durations and probabilites.
39+
'''
40+
if len(statusses) != len(probabilites) or len(durations) != len(probabilites):
41+
log.info(
42+
"Length of statusses, duration and probabilites does not match. Set status to up")
43+
return "up", 0
44+
choice = choices([i for i in range(len(probabilites))], probabilites)[0]
45+
return statusses[choice], durations[choice]
46+
3747
cumulocityAPI = CumulocityAPI()
3848
oeeAPI = OeeAPI()
3949

@@ -60,8 +70,10 @@ def __calculate_next_run(self) -> int:
6070
return time.time() + randint(self.min_interval_in_seconds, self.max_interval_in_seconds)
6171

6272
def __reschedule_and_run(self):
63-
self.next_run = self.__calculate_next_run()
64-
self.run_block(self)
73+
duration = self.run_block(self)
74+
duration = duration.pop() or 0 # Why is duration a set?
75+
self.next_run = self.__calculate_next_run() + duration
76+
log.debug(f"Reschedule next run and wait for additional {duration} seconds. Next run is at {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.next_run))}")
6577

6678
def tick(self):
6779
if (time.time() - self.next_run) > 0:
@@ -133,14 +145,13 @@ def __on_availability_event(self, event_definition, task):
133145

134146
event = self.__type_fragment(event_definition)
135147

136-
status_up_probability = event_definition.get("statusUpProbability") or 0.5
137-
138-
if try_event(status_up_probability):
139-
event.update({'status': 'up'})
140-
self.machine_up = True
141-
else:
142-
event.update({'status': 'down'})
143-
self.machine_up = False
148+
statusses = event_definition.get("status") or ["up"]
149+
probabilities = event_definition.get("probabilities") or [0.5]
150+
durations = event_definition.get("durations") or [0]
151+
status, duration = get_random_status(
152+
statusses, durations, probabilities)
153+
self.machine_up = (status == "up")
154+
event.update({'status': status})
144155

145156
event.update(self.__get_production_info())
146157
self.__send_event(event)
@@ -150,6 +161,8 @@ def __on_availability_event(self, event_definition, task):
150161
if self.__is_whole_piece_available():
151162
self.__force_production_event()
152163

164+
return duration
165+
153166
def __force_production_event(self):
154167
for event_definition in self.model["events"]:
155168
event_type = event_definition.get("type")
@@ -358,6 +371,7 @@ def load(filename):
358371
if CREATE_PROFILES.lower() == "true":
359372
[oeeAPI.create_and_activate_profile(id, ProfileCreateMode.CREATE_IF_NOT_EXISTS)
360373
for id in oeeAPI.get_simulator_external_ids()]
374+
os.system("python profile_generator.py -cat")
361375

362376
while True:
363377
for simulator in simulators:

event-based-simulators/main/profile_generator.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def try_int(value):
2222
help="remove all simulator profiles using the OEE API provided by oee-bundle")
2323
group.add_argument("-d", "--delete-simulator-profiles", action="store_true", dest="deleteSimulatorProfiles",
2424
help="delete all simulator profiles using the C8Y inventory API (useful if oee-bundle is not working/available")
25+
group.add_argument("-cat", "--create-categories", action="store_true", dest="createCalculationCategories", help="create or update calclation categories")
26+
group.add_argument("--delete-categories", action="store_true", dest="deleteCalculationCategories", help="delete all calclation categories")
2527
args = parser.parse_args()
2628

2729
# JSON-PYTHON mapping, to get json.load() working
@@ -86,3 +88,28 @@ def delete_profiles():
8688
delete_profiles()
8789
log.info(f'profiles after execution: {c8y_api.count_all_profiles()}')
8890

91+
if args.deleteCalculationCategories:
92+
log.info('===================================')
93+
log.info('starting to delete all calculation categories ...')
94+
log.info(
95+
f'existing category managed objects: {c8y_api.count_all_categories()}')
96+
deleted_categories = 0
97+
for category in c8y_api.get_calculation_categories():
98+
deleted_categories += c8y_api.delete_managed_object(category['id'])
99+
log.info(f'Managed_objects deleted: {deleted_categories}')
100+
101+
if args.createCalculationCategories:
102+
log.info('===================================')
103+
log.info('starting to create calculation categories ...')
104+
with open('./categories.json', 'r') as f:
105+
categories = f.read()
106+
if (c8y_api.count_all_categories()) == 0:
107+
log.info('Create category managed object')
108+
c8y_api.create_managed_object(categories)
109+
elif (c8y_api.count_all_categories()) == 1:
110+
log.info('Update category managed object')
111+
c8y_category = c8y_api.get_calculation_categories()[0]
112+
c8y_api.update_managed_object(c8y_category['id'], categories)
113+
else:
114+
log.warning('More than 1 category managed object! Unable to update managed object')
115+
log.info('==========Categories created==========')
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"id": "",
3+
"alarms": [],
4+
"type": "MACHINE",
5+
"deviceId": "${deviceId}",
6+
"shortShutdowns": null,
7+
"version": "2.0.0",
8+
"amountUnit": "",
9+
"name": "Ideal producer with planned maintenance and manual stops",
10+
"tenantId": "${tenantId}",
11+
"locationId": "Matrix",
12+
"external": false,
13+
"testConfiguration": false,
14+
"subscriptions": [
15+
{
16+
"type": "event",
17+
"name": "Availability",
18+
"deviceId": null,
19+
"pollingIntervalSeconds": 10,
20+
"key": "event.Availability"
21+
},
22+
{
23+
"type": "event",
24+
"name": "Pieces_Ok",
25+
"deviceId": null,
26+
"pollingIntervalSeconds": 10,
27+
"key": "event.Pieces_Ok"
28+
},
29+
{
30+
"type": "event",
31+
"name": "Pieces_Produced",
32+
"deviceId": null,
33+
"pollingIntervalSeconds": 10,
34+
"key": "event.Pieces_Produced"
35+
}
36+
],
37+
"inputs": {
38+
"AvailabilityLossTime": [
39+
{
40+
"type": "MACHINE_EVENT",
41+
"category": "1",
42+
"value": ""
43+
},
44+
{
45+
"type": "MACHINE_EVENT",
46+
"category": "2",
47+
"value": ""
48+
}
49+
],
50+
"ActualQualityAmount": [
51+
{
52+
"type": "TRANSFORMATION_RULE",
53+
"value": "countEvents(\"${deviceId}\",\"Pieces_Ok\")"
54+
}
55+
],
56+
"ActualProductionAmount": [
57+
{
58+
"type": "TRANSFORMATION_RULE",
59+
"value": "countEvents(\"${deviceId}\",\"Pieces_Produced\")"
60+
}
61+
]
62+
},
63+
"machineEvents": {
64+
"MACHINE_STATUS": [
65+
{
66+
"category": "1",
67+
"value": "evt(\"${deviceId}\",\"Availability\",\"status\",false) = \"Planned maintenance\""
68+
},
69+
{
70+
"category": "2",
71+
"value": "evt(\"${deviceId}\",\"Availability\",\"source\",false) = \"Manual stop\""
72+
}
73+
],
74+
"QUALITY_STATUS": []
75+
},
76+
"oeeTargets": {
77+
"performance": 80,
78+
"overall": 70,
79+
"availability": 80,
80+
"quality": 100
81+
},
82+
"workpiece": {
83+
"amount": "300",
84+
"unit": "pcs",
85+
"name": "Pieces",
86+
"isActive": true,
87+
"timeunit": 2
88+
},
89+
"timeframes": {
90+
"Interval3": null,
91+
"IntervalUnit2": "h",
92+
"Interval4": null,
93+
"IntervalUnit1": "min",
94+
"Interval1": "10",
95+
"IntervalUnit4": null,
96+
"Interval2": "1",
97+
"IntervalUnit3": null
98+
},
99+
"ui": {
100+
"shortStoppages": "",
101+
"correlationSectionVisited": false,
102+
"computation": "LPQ",
103+
"shortStoppagesAmount": "1"
104+
},
105+
"intervals": [
106+
600.0,
107+
3600.0
108+
],
109+
"status": "INCOMPLETE"
110+
}

0 commit comments

Comments
 (0)