diff --git a/components/entitiy_ident/entity_ident_service/entity_ident_server.py b/components/entitiy_ident/entity_ident_service/entity_ident_server.py index 3409e5e..23587b5 100644 --- a/components/entitiy_ident/entity_ident_service/entity_ident_server.py +++ b/components/entitiy_ident/entity_ident_service/entity_ident_server.py @@ -1,3 +1,4 @@ +import os from bson import json_util from flask import Flask, request from flask_pymongo import PyMongo @@ -6,6 +7,38 @@ app = Flask(__name__) app.config["MONGO_URI"] = "mongodb://mongo:27017/entities" mongo = PyMongo(app) +CAR1_SV = int(os.environ.get('DSE2021_CAR1_SV', 130)) +CAR2_SV = int(os.environ.get('DSE2021_CAR2_SV', 130)) +CAR3_SV = int(os.environ.get('DSE2021_CAR3_SV', 130)) + +CAR1_SD = int(os.environ.get('DSE2021_CAR1_SD', 300)) +CAR2_SD = int(os.environ.get('DSE2021_CAR2_SD', 500)) +CAR3_SD = int(os.environ.get('DSE2021_CAR3_SD', 400)) + +CAR1_ST = int(os.environ.get('DSE2021_CAR1_ST', 10)) +CAR2_ST = int(os.environ.get('DSE2021_CAR2_ST', 15)) +CAR3_ST = int(os.environ.get('DSE2021_CAR3_ST', 25)) + +TL1_R = int(os.environ.get('DSE2021_TL1_R', 2000)) +TL2_R = int(os.environ.get('DSE2021_TL2_R', 800)) +TL3_R = int(os.environ.get('DSE2021_TL3_R', 1000)) + +mongo.db.trafficLights.update_one({"id": "1"}, {"$set": {"range": TL1_R}}) +mongo.db.trafficLights.update_one({"id": "2"}, {"$set": {"range": TL2_R}}) +mongo.db.trafficLights.update_one({"id": "3"}, {"$set": {"range": TL3_R}}) + +mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingVelocity": CAR1_SV}}) +mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingVelocity": CAR2_SV}}) +mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingVelocity": CAR3_SV}}) + +mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingDistance": CAR1_SD}}) +mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingDistance": CAR2_SD}}) +mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingDistance": CAR3_SD}}) + +mongo.db.cars.update_one({"vin": "SCBFR7ZA5CC072256"}, {"$set": {"startingTime": CAR1_ST}}) +mongo.db.cars.update_one({"vin": "5GZCZ43D13S812715"}, {"$set": {"startingTime": CAR2_ST}}) +mongo.db.cars.update_one({"vin": "5GZCZ43D13S812716"}, {"$set": {"startingTime": CAR3_ST}}) + @app.route('/api/v1/resources/cars', methods=['GET']) def get_cars(): diff --git a/components/entitiy_ident/mongo/cars.json b/components/entitiy_ident/mongo/cars.json index 363fe74..b6d5a87 100644 --- a/components/entitiy_ident/mongo/cars.json +++ b/components/entitiy_ident/mongo/cars.json @@ -2,25 +2,16 @@ { "oem": "BENTLEY", "modelType": "Continental", - "vin": "SCBFR7ZA5CC072256", - "startingVelocity": 130, - "startingDistance": 300, - "startingTime": 10 + "vin": "SCBFR7ZA5CC072256" }, { "oem": "SATURN", "modelType": "Vue", - "vin": "5GZCZ43D13S812715", - "startingVelocity": 130, - "startingDistance": 500, - "startingTime": 15 + "vin": "5GZCZ43D13S812715" }, { "oem": "SATURN", "modelType": "Vue2", - "vin": "5GZCZ43D13S812716", - "startingVelocity": 130, - "startingDistance": 100, - "startingTime": 25 + "vin": "5GZCZ43D13S812716" } ] \ No newline at end of file diff --git a/components/entitiy_ident/mongo/traffic_lights.json b/components/entitiy_ident/mongo/traffic_lights.json index 0fbe8e3..6f83730 100644 --- a/components/entitiy_ident/mongo/traffic_lights.json +++ b/components/entitiy_ident/mongo/traffic_lights.json @@ -2,22 +2,19 @@ { "id": "1", "location": [16.20719, 47.89584], - "range": 2000, - "switchingTime": 5, + "switchingTime": 26, "color": "RED" }, { "id": "2", "location": [16.20814, 47.90937], - "range": 800, - "switchingTime": 15, + "switchingTime": 16, "color": "GREEN" }, { "id": "3", "location": [16.20917, 47.92703], - "range": 1000, - "switchingTime": 10, + "switchingTime": 20, "color": "RED" } ] \ No newline at end of file diff --git a/components/event_store/service/test_event_logger.py b/components/event_store/service/test_event_logger.py index ac13786..3128283 100644 --- a/components/event_store/service/test_event_logger.py +++ b/components/event_store/service/test_event_logger.py @@ -16,7 +16,7 @@ from event_logger import EventLogger class TestEventLogger(unittest.TestCase): def setUp(self) -> None: self.el = EventLogger(StrictRedis(), False, False) - self.timestamp = datetime.datetime.now() + self.timestamp = datetime.datetime.utcnow() def test_unpack_daf(self): daf = DAF(vehicle_identification_number='my_vin', @@ -60,7 +60,7 @@ class TestEventLogger(unittest.TestCase): self.assertEqual(message, json.dumps(unknown)) def test_unpack_unknown_object(self): - obj = datetime.datetime.now() + obj = datetime.datetime.utcnow() key, message = self.el._unpack_message_to_log(pickle.dumps(obj)) diff --git a/components/i_feed/devices/traffic_light.py b/components/i_feed/devices/traffic_light.py index 3b3c7e8..429d6c5 100644 --- a/components/i_feed/devices/traffic_light.py +++ b/components/i_feed/devices/traffic_light.py @@ -1,6 +1,7 @@ import pickle import threading import time +import os from datetime import datetime from circuitbreaker import circuit @@ -13,7 +14,7 @@ from pika.exceptions import AMQPConnectionError SWITCHING_TIME = 15 # Scale speed of switching by factor x -SCALING = 1 +SCALING = int(os.environ.get('DSE2021_SCALING', 1)) class TrafficLight: @@ -56,23 +57,10 @@ class TrafficLight: def start(self): """ - Starts the traffic light by spawning a new thread. It toggles its state every self.switching_time / SCALING - seconds. When it switches, it notifies the orchestrator with self.send_status_update(). + Starts the traffic light by spawning a new thread.. """ self.running = True - - def switcher(): - num_colors = len(TrafficLightColor) - # set current color to the one before starting color, because switch immediately happens in loop - # therefore it sleeps on the end of the loop and first status is immediately sent to msg broker - self.current_color = TrafficLightColor((self._starting_color.value - 1) % num_colors) - while self.running: - self.current_color = TrafficLightColor((self.current_color.value + 1) % num_colors) - self.last_switch = datetime.now() - self.send_status_update() - time.sleep(self.switching_time / SCALING) - - self._t = threading.Thread(target=switcher) + self._t = threading.Thread(target=self._switcher) self._t.start() def stop(self): @@ -91,6 +79,21 @@ class TrafficLight: self._tl_mb.send(pickle.dumps( TrafficLightState(tlid=self.tlid, color=self.current_color, last_switch=self.last_switch))) + def _switcher(self): + """ + Toggles the traffic lights state every self.switching_time / SCALING seconds. + When it switches, it notifies the orchestrator with self.send_status_update() + """ + num_colors = len(TrafficLightColor) + # set current color to the one before starting color, because switch immediately happens in loop + # therefore it sleeps on the end of the loop and first status is immediately sent to msg broker + self.current_color = TrafficLightColor((self._starting_color.value - 1) % num_colors) + while self.running: + self.current_color = TrafficLightColor((self.current_color.value + 1) % num_colors) + self.last_switch = datetime.utcnow() + self.send_status_update() + time.sleep(self.switching_time / SCALING) + if __name__ == '__main__': ... diff --git a/components/i_feed/devices/vehicle.py b/components/i_feed/devices/vehicle.py index d4b55d5..4856273 100644 --- a/components/i_feed/devices/vehicle.py +++ b/components/i_feed/devices/vehicle.py @@ -1,6 +1,7 @@ import pickle import threading import time +import os from datetime import datetime from typing import Union @@ -19,11 +20,13 @@ STARTING_POINT = geopy.Point(47.89053, 16.20703) # Driving direction in degrees: 0=N, 90=E, 180=S, 270=W BEARING = 2 # Scale speed of vehicles by factor x -SCALING = 1 +SCALING = int(os.environ.get('DSE2021_SCALING', 1)) # in km/h STARTING_VELOCITY = 130 * SCALING # Interval between status updates in seconds (is not scaled) UPDATE_INTERVAL = 1 +# Specify if NCE should happen +NCE = int(os.environ.get('DSE2021_NCE', 1)) # At x km the NCE shall happen NCE_KM = 2.4 # Time in seconds to recover from NCE (will be scaled) @@ -104,8 +107,12 @@ class Vehicle: :return: True if NCE invoked, otherwise False """ - if self._nce_possible and not self._nce_happened: - if self._driven_kms >= NCE_KM: + d = geopy.distance.distance(kilometers=0.4) + tl2_loc = geopy.Point(47.90937, 16.20814) + # Calculate point 400m south of traffic light 2 + nce_point = d.destination(point=tl2_loc, bearing=182) + if NCE == 1 and self._nce_possible and not self._nce_happened: + if self._gps_location.latitude >= nce_point.latitude: self._nce_happened = True self._last_velocity = self.velocity self.velocity = 0 @@ -163,14 +170,14 @@ class Vehicle: # Get old and updated timestamps old_timestamp = self.last_update - updated_timestamp = datetime.now() + updated_timestamp = datetime.utcnow() self.last_update = updated_timestamp # get driving time between timestamps (in seconds) driving_time = (updated_timestamp - old_timestamp).total_seconds() # reached distance in kilometers: convert km/h to km/s and multiply by driving time - kilometers = self.velocity / 3600 * driving_time * SCALING + kilometers = self.velocity / 3600 * driving_time # Define a general distance object, initialized with a distance of k km. d = geopy.distance.distance(kilometers=kilometers) @@ -196,7 +203,7 @@ class Vehicle: informs the message broker about the current state (DAF) of the vehicle. """ print('{} starts driving ... SCALING: x{}\n\n'.format(self.vin, SCALING)) - self.last_update = datetime.now() + self.last_update = datetime.utcnow() self._driven_kms = 0 self._t = threading.Thread(target=self.drive) @@ -223,14 +230,16 @@ class Vehicle: def check_reset(self): """ Checks if end of route is reached and resets vehicle to starting conditions. + It also resets on malicious coordinates. + Afterwards, the vehicle is still driving, but again from start. """ if self._driven_kms >= RESET_KM: - print('\n\nEnd of route reached ... resetting and restarting vehicle') - self._gps_location = self._starting_point - self._driven_kms = 0 - self.last_update = datetime.now() - self.nce = False + self._reset() + + if self._gps_location.latitude < self._starting_point.latitude or \ + self._gps_location.longitude < self._starting_point.longitude: + self._reset() @circuit(failure_threshold=10, expected_exception=AMQPConnectionError) def send_status_update(self): @@ -255,6 +264,14 @@ class Vehicle: else: print('We are still recovering ... ignoring new target velocity.') + def _reset(self): + print('\n\nEnd of route reached ... resetting and restarting vehicle') + self._gps_location = self._starting_point + self._driven_kms = 0 + self.last_update = datetime.utcnow() + self.nce = False + self.velocity = STARTING_VELOCITY + if __name__ == "__main__": ... diff --git a/components/i_feed/test_ifeed.py b/components/i_feed/test_ifeed.py new file mode 100644 index 0000000..8faeba9 --- /dev/null +++ b/components/i_feed/test_ifeed.py @@ -0,0 +1,113 @@ +import datetime +import pickle +import time +import unittest +from unittest.mock import patch + +import geopy +from dse_shared_libs.daf import DAF +from dse_shared_libs.mock.datetime import MyDate +from dse_shared_libs.target_velocity import TargetVelocity +from dse_shared_libs.traffic_light_color import TrafficLightColor + +from devices.traffic_light import TrafficLight +from devices.vehicle import Vehicle +from dse_shared_libs.mock.rabbit_mq_mocks import MyBlockingConnection + + +class TestVehicle(unittest.TestCase): + def setUp(self) -> None: + with patch('pika.BlockingConnection', MyBlockingConnection): + self.vin = 'my_vin' + self.starting_velocity = 130 + self.starting_point = geopy.Point(0, 0, 0) + self.timestamp = datetime.datetime.utcnow() + + self.v = Vehicle( + vin=self.vin, + starting_point=self.starting_point, + starting_velocity=self.starting_velocity + ) + + self.v.last_update = self.timestamp + self.v._driven_kms = 0 + + def tearDown(self) -> None: + time.sleep(0.1) + + def test_nce_prop(self): + with patch('time.sleep', return_value=None): + # initially false + self.assertEqual(self.v.nce, False) + # get past nce km + self.v._gps_location.latitude = 48 + # now nce should fire + self.assertEqual(self.v.nce, True) + # second call is false again + self.assertEqual(self.v.nce, False) + + def test_daf_prop(self): + with patch('devices.vehicle.datetime', MyDate): + MyDate.set_timestamp(self.timestamp) + + # test if daf object created properly + daf = DAF(vehicle_identification_number=self.vin, + gps_location=self.starting_point, + velocity=self.starting_velocity, + timestamp=self.timestamp, + near_crash_event=False) + self.assertEqual(self.v.daf, daf) + + def test_current_location_prop(self): + with patch('devices.vehicle.datetime', MyDate): + MyDate.set_timestamp(self.timestamp) + + # initial position should be starting point + self.assertEqual(self.v.gps_location, self.starting_point) + + # lets say last time was about an hour ago + self.v.last_update = self.timestamp - datetime.timedelta(hours=1) + + # we drive "almost" directly north + loc = self.v.gps_location + self.assertEqual(round(loc.latitude, 2), 1.17) + self.assertEqual(round(loc.longitude, 2), 0.04) + + def test_driven_kms_prop(self): + # initial driven kms are 0 + self.assertEqual(self.v.driven_kms, '0km') + + # alter driven kms + self.v._driven_kms = 44.24552 + # will be rounded to 2 digits + self.assertEqual(self.v.driven_kms, '44.25km') + + def test_new_velocity(self): + # starting velocity is set to 130 + self.assertEqual(self.v.velocity, 130) + + # we get a new velocity, lets say 55 ... + tv = TargetVelocity(vin='my_vin', target_velocity=55, timestamp=self.timestamp) + self.v.new_velocity(pickle.dumps(tv)) + # then the vehicle should comply with that + self.assertEqual(self.v.velocity, 55) + + +class TestTrafficLight(unittest.TestCase): + def setUp(self) -> None: + with patch('pika.BlockingConnection', MyBlockingConnection): + self.tl = TrafficLight(tlid='my_tl', + switching_time=1, + starting_color=TrafficLightColor.RED) + + def tearDown(self) -> None: + self.tl.stop() + + def test_switching(self): + self.assertFalse(hasattr(self.tl, 'current_color')) + self.tl.start() + self.assertEqual(self.tl.current_color, TrafficLightColor.RED) + time.sleep(1.2) + self.assertEqual(self.tl.current_color, TrafficLightColor.GREEN) + time.sleep(1) + self.assertEqual(self.tl.current_color, TrafficLightColor.RED) diff --git a/components/orchestration/orchestrator.py b/components/orchestration/orchestrator.py index 5277a01..acee465 100644 --- a/components/orchestration/orchestrator.py +++ b/components/orchestration/orchestrator.py @@ -1,9 +1,10 @@ import datetime import pickle import sys +import os from typing import List, Dict from datetime import datetime, timedelta -from math import floor, ceil +from math import floor import requests from dse_shared_libs import daf, traffic_light_state, traffic_light_color, target_velocity @@ -22,7 +23,7 @@ sys.modules['target_velocity'] = target_velocity ENTITY_IDENT_URL = 'http://entityident:5002/api/v1/resources/' -SCALING = 1 +SCALING = int(os.environ.get('DSE2021_SCALING', 1)) class Orchestrator: @@ -60,7 +61,7 @@ class Orchestrator: for traffic_light in traffic_lights['cursor']: self.tls[traffic_light['id']] = {'color': traffic_light['color'], 'switching_time': traffic_light['switchingTime'], - 'last_switch': datetime.now()} + 'last_switch': datetime.utcnow()} def setup_msg_queues(self): """ @@ -88,7 +89,7 @@ class Orchestrator: """ Gets the daf object's pickle binary dump. Unpickle, calculate new target velocity based on daf and current tl data and - respond new target velicity for this vehicle. + respond new target velocity for this vehicle. :param pickle_binary: daf object pickle binary dump """ @@ -100,55 +101,14 @@ class Orchestrator: params={'lat': loc.latitude, 'lon': loc.longitude}) traffic_lights_geo = response.json() current_vel = received_daf_object.velocity - target_vel = 130 * SCALING - print('Nearest traffic lights: {}'.format(traffic_lights_geo['cursor'])) - for traffic_light in traffic_lights_geo['cursor']: - # Should only ever contain one traffic light (the next in line) - tl_id = traffic_light['id'] - distance = traffic_light['calculatedRange'] - switching_time = self.tls[tl_id]['switching_time'] / float(SCALING) - # Time until next switch must be scaled accordingly - next_switch_time = self.tls[tl_id]['last_switch'] + timedelta(seconds=switching_time) - time_until_switch = (next_switch_time - datetime.now()).total_seconds() - print('Distance to TL: {}'.format(distance)) - print('Time until switch in seconds: {}'.format(time_until_switch)) - if self.tls[tl_id]['color'] is TrafficLightColor.RED: - speed_needed_max = (distance / float(time_until_switch)) * 3.6 - if speed_needed_max < 130 * SCALING: - if current_vel < speed_needed_max: - target_vel = current_vel - else: - target_vel = floor(speed_needed_max) - else: - # Cannot make it on next green - i = 2 - while speed_needed_max > 130 * SCALING: - next_green_phase_start = time_until_switch + switching_time * i - speed_needed_max = (distance / float(next_green_phase_start)) * 3.6 - i = i + 2 - target_vel = floor(speed_needed_max) - else: - # Check if we can reach TL in time - speed_needed_min = (distance / float(time_until_switch)) * 3.6 - if speed_needed_min < 130 * SCALING: - if current_vel < speed_needed_min: - target_vel = 130 * SCALING - else: - target_vel = current_vel - else: - i = 1 - speed_needed_max = 132 * SCALING - while speed_needed_max > 130 * SCALING: - next_green_phase_start = time_until_switch + switching_time * i - speed_needed_max = (distance / float(next_green_phase_start)) * 3.6 - i = i + 2 - target_vel = floor(speed_needed_max) + + target_vel = self._compute_velocity(traffic_lights_geo, current_vel) response_channel = self._velocity_mbs[received_daf_object.vehicle_identification_number] print('Target velocity: {}'.format(target_vel)) response_channel.send(pickle.dumps( TargetVelocity(vin=received_daf_object.vehicle_identification_number, target_velocity=target_vel, - timestamp=datetime.now()))) + timestamp=datetime.utcnow()))) def handle_tl_state_receive(self, msg): """ @@ -164,3 +124,55 @@ class Orchestrator: self.tls[tl_state.tlid]['color'] = tl_state.color self.tls[tl_state.tlid]['last_switch'] = tl_state.last_switch print(tl_state) + + def _compute_velocity(self, traffic_lights_geo, current_vel): + target_vel = 130 * SCALING + + print('Nearest traffic lights: {}'.format(traffic_lights_geo['cursor'])) + for traffic_light in traffic_lights_geo['cursor']: + # Should only ever contain one traffic light (the next in line) + tl_id = traffic_light['id'] + distance = traffic_light['calculatedRange'] + switching_time = self.tls[tl_id]['switching_time'] / float(SCALING) + # Time until next switch must be scaled accordingly + next_switch_time = self.tls[tl_id]['last_switch'] + timedelta(seconds=switching_time) + time_until_switch = (next_switch_time - datetime.utcnow()).total_seconds() + print('Distance to TL: {}'.format(distance)) + print('Time until switch in seconds: {}'.format(time_until_switch)) + + if self.tls[tl_id]['color'] is TrafficLightColor.RED: + speed_needed_max = (distance / float(time_until_switch)) * 3.6 + if speed_needed_max < 130 * SCALING: + if current_vel < speed_needed_max: + target_vel = current_vel + else: + target_vel = floor(speed_needed_max) + + else: + # Cannot make it on next green + i = 2 + while speed_needed_max > 130 * SCALING: + next_green_phase_start = time_until_switch + switching_time * i + speed_needed_max = (distance / float(next_green_phase_start)) * 3.6 + i = i + 2 + target_vel = floor(speed_needed_max) + + elif self.tls[tl_id]['color'] is TrafficLightColor.GREEN: + # Check if we can reach TL in time + speed_needed_min = (distance / float(time_until_switch)) * 3.6 + if speed_needed_min < 130 * SCALING: + if current_vel < speed_needed_min: + target_vel = 130 * SCALING + else: + target_vel = current_vel + else: + i = 1 + speed_needed_max = 132 * SCALING + while speed_needed_max > 130 * SCALING: + next_green_phase_start = time_until_switch + switching_time * i + speed_needed_max = (distance / float(next_green_phase_start)) * 3.6 + i = i + 2 + target_vel = floor(speed_needed_max) + else: + print('Unknown Traffic Light Color!') + return target_vel diff --git a/components/orchestration/test_orchestrator.py b/components/orchestration/test_orchestrator.py new file mode 100644 index 0000000..50ad708 --- /dev/null +++ b/components/orchestration/test_orchestrator.py @@ -0,0 +1,54 @@ +import datetime +import unittest +from unittest.mock import patch + +from dse_shared_libs.mock.response import MyResponse +from dse_shared_libs.traffic_light_color import TrafficLightColor + +from orchestrator import Orchestrator + + +class TestOrchestrator(unittest.TestCase): + def setUp(self) -> None: + self.timestamp = datetime.datetime.utcnow() + with patch('requests.sessions.Session.get', MyResponse): + self.orc = Orchestrator() + + def test_full_speed_if_nothing_in_range(self): + tl_geo = {'cursor': []} + current_vel = 55.0 + + target_vel = self.orc._compute_velocity(tl_geo, current_vel) + self.assertEqual(130, target_vel) + + def test_keep_speed_if_far_away_and_green(self): + self.orc.tls = {'1': {'color': TrafficLightColor.GREEN, 'switching_time': 1000, 'last_switch': self.timestamp}} + tl_geo = {'cursor': [ + {'id': '1', 'calculatedRange': 1000} + ]} + current_vel = 55.0 + + target_vel = self.orc._compute_velocity(tl_geo, current_vel) + self.assertEqual(current_vel, target_vel) + + def test_slow_down_if_passing_RED_not_possible(self): + self.orc.tls = {'1': {'color': TrafficLightColor.RED, 'switching_time': 100000, 'last_switch': self.timestamp}} + tl_geo = {'cursor': [ + {'id': '1', 'calculatedRange': 1} + ]} + current_vel = 130.0 + + target_vel = self.orc._compute_velocity(tl_geo, current_vel) + self.assertNotEqual(current_vel, target_vel) + self.assertEqual(0, target_vel) + + def test_adjust_speed_to_get_over_on_green_if_currently_red(self): + self.orc.tls = {'1': {'color': TrafficLightColor.RED, 'switching_time': 100, 'last_switch': self.timestamp}} + tl_geo = {'cursor': [ + {'id': '1', 'calculatedRange': 1000} + ]} + current_vel = 130.0 + + target_vel = self.orc._compute_velocity(tl_geo, current_vel) + self.assertNotEqual(current_vel, target_vel) + self.assertEqual(36.0, target_vel) diff --git a/components/shared/dse_shared_libs/mock/__init__.py b/components/shared/dse_shared_libs/mock/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/shared/dse_shared_libs/mock/datetime.py b/components/shared/dse_shared_libs/mock/datetime.py new file mode 100644 index 0000000..a1db7c7 --- /dev/null +++ b/components/shared/dse_shared_libs/mock/datetime.py @@ -0,0 +1,10 @@ +class MyDate: + timestamp = None + + @classmethod + def set_timestamp(cls, timestamp): + cls.timestamp = timestamp + + @classmethod + def utcnow(cls): + return cls.timestamp diff --git a/components/shared/dse_shared_libs/mock/rabbit_mq_mocks.py b/components/shared/dse_shared_libs/mock/rabbit_mq_mocks.py new file mode 100644 index 0000000..a6a551d --- /dev/null +++ b/components/shared/dse_shared_libs/mock/rabbit_mq_mocks.py @@ -0,0 +1,43 @@ +class MyQueue: + @property + def queue(self): + return None + + +class MyMethod: + method = MyQueue() + + +class MyChannel: + @staticmethod + def exchange_declare(*args, **kwargs): + return None + + @staticmethod + def queue_declare(*args, **kwargs): + return MyMethod + + @staticmethod + def queue_bind(*args, **kwargs): + return None + + @staticmethod + def basic_consume(*args, **kwargs): + return None + + @staticmethod + def basic_publish(*args, **kwargs): + return None + + @staticmethod + def start_consuming(*args, **kwargs): + return None + + +class MyBlockingConnection: + def __init__(self, *args, **kwargs): + ... + + @staticmethod + def channel(): + return MyChannel() diff --git a/components/shared/dse_shared_libs/mock/response.py b/components/shared/dse_shared_libs/mock/response.py new file mode 100644 index 0000000..93e33d5 --- /dev/null +++ b/components/shared/dse_shared_libs/mock/response.py @@ -0,0 +1,7 @@ +class MyResponse: + def __init__(self, *args, **kwargs): + ... + + @staticmethod + def json(*args, **kwargs): + return {'cursor': []} diff --git a/components/x_way/requirements.txt b/components/x_way/requirements.txt index ec961a5..b09968e 100644 --- a/components/x_way/requirements.txt +++ b/components/x_way/requirements.txt @@ -1,4 +1,4 @@ -flask +flask==1.1.4 Flask-Cors requests Flask-PyMongo diff --git a/components/x_way/x_way_server.py b/components/x_way/x_way_server.py index 47e1fdb..007db8c 100644 --- a/components/x_way/x_way_server.py +++ b/components/x_way/x_way_server.py @@ -5,7 +5,7 @@ from bson import json_util from flask import Flask, jsonify from flask_cors import CORS from flask import request -import json; +import json app = Flask(__name__) CORS(app) diff --git a/kubernetes/configmaps/scaling-configmap.yaml b/kubernetes/configmaps/scaling-configmap.yaml deleted file mode 100644 index a2de7d1..0000000 --- a/kubernetes/configmaps/scaling-configmap.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: scaling-configmap -data: - scaling: "1" \ No newline at end of file diff --git a/kubernetes/configmaps/simulation-parameters-configmap.yaml b/kubernetes/configmaps/simulation-parameters-configmap.yaml new file mode 100644 index 0000000..f28712b --- /dev/null +++ b/kubernetes/configmaps/simulation-parameters-configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: simulation-parameters-configmap +data: + DSE2021_SCALING: "2" + DSE2021_NCE: "1" + DSE2021_CAR1_SV: "130" + DSE2021_CAR2_SV: "130" + DSE2021_CAR3_SV: "130" + DSE2021_CAR1_SD: "300" + DSE2021_CAR2_SD: "500" + DSE2021_CAR3_SD: "400" + DSE2021_CAR1_ST: "10" + DSE2021_CAR2_ST: "15" + DSE2021_CAR3_ST: "25" + DSE2021_TL1_R: "2000" + DSE2021_TL2_R: "800" + DSE2021_TL3_R: "1000" \ No newline at end of file diff --git a/kubernetes/deployments/controlcenter-deployment.yaml b/kubernetes/deployments/controlcenter-deployment.yaml index 3ab29b4..596e1e3 100644 --- a/kubernetes/deployments/controlcenter-deployment.yaml +++ b/kubernetes/deployments/controlcenter-deployment.yaml @@ -23,11 +23,11 @@ spec: - containerPort: 80 resources: {} env: - - name: SCALING + - name: DSE2021_SCALING valueFrom: configMapKeyRef: - name: scaling-configmap - key: scaling + name: simulation-parameters-configmap + key: DSE2021_SCALING restartPolicy: Always serviceAccountName: "" volumes: null diff --git a/kubernetes/deployments/entityident-deployment.yaml b/kubernetes/deployments/entityident-deployment.yaml index 2c894e3..afcbd65 100644 --- a/kubernetes/deployments/entityident-deployment.yaml +++ b/kubernetes/deployments/entityident-deployment.yaml @@ -23,11 +23,71 @@ spec: - containerPort: 5002 resources: {} env: - - name: SCALING + - name: DSE2021_SCALING valueFrom: configMapKeyRef: - name: scaling-configmap - key: scaling + name: simulation-parameters-configmap + key: DSE2021_SCALING + - name: DSE2021_CAR1_SV + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR1_SV + - name: DSE2021_CAR2_SV + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR2_SV + - name: DSE2021_CAR3_SV + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR3_SV + - name: DSE2021_CAR1_SD + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR1_SD + - name: DSE2021_CAR2_SD + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR2_SD + - name: DSE2021_CAR3_SD + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR3_SD + - name: DSE2021_CAR1_ST + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR1_ST + - name: DSE2021_CAR2_ST + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR2_ST + - name: DSE2021_CAR3_ST + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_CAR3_ST + - name: DSE2021_TL1_R + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_TL1_R + - name: DSE2021_TL2_R + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_TL2_R + - name: DSE2021_TL3_R + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_TL3_R restartPolicy: Always serviceAccountName: "" volumes: null diff --git a/kubernetes/deployments/eventstore-deployment.yaml b/kubernetes/deployments/eventstore-deployment.yaml index a38908f..cb10349 100644 --- a/kubernetes/deployments/eventstore-deployment.yaml +++ b/kubernetes/deployments/eventstore-deployment.yaml @@ -23,11 +23,11 @@ spec: - containerPort: 5001 resources: {} env: - - name: SCALING + - name: DSE2021_SCALING valueFrom: configMapKeyRef: - name: scaling-configmap - key: scaling + name: simulation-parameters-configmap + key: DSE2021_SCALING restartPolicy: Always serviceAccountName: "" volumes: null diff --git a/kubernetes/deployments/ifeed-deployment.yaml b/kubernetes/deployments/ifeed-deployment.yaml index 981cc7c..2e00553 100644 --- a/kubernetes/deployments/ifeed-deployment.yaml +++ b/kubernetes/deployments/ifeed-deployment.yaml @@ -21,11 +21,16 @@ spec: name: ifeed resources: {} env: - - name: SCALING + - name: DSE2021_SCALING valueFrom: configMapKeyRef: - name: scaling-configmap - key: scaling + name: simulation-parameters-configmap + key: DSE2021_SCALING + - name: DSE2021_NCE + valueFrom: + configMapKeyRef: + name: simulation-parameters-configmap + key: DSE2021_NCE restartPolicy: Always serviceAccountName: "" volumes: null diff --git a/kubernetes/deployments/orchestration-deployment.yaml b/kubernetes/deployments/orchestration-deployment.yaml index a71e304..21bff31 100644 --- a/kubernetes/deployments/orchestration-deployment.yaml +++ b/kubernetes/deployments/orchestration-deployment.yaml @@ -21,11 +21,11 @@ spec: name: orchestration resources: {} env: - - name: SCALING + - name: DSE2021_SCALING valueFrom: configMapKeyRef: - name: scaling-configmap - key: scaling + name: simulation-parameters-configmap + key: DSE2021_SCALING restartPolicy: Always serviceAccountName: "" volumes: null diff --git a/kubernetes/deployments/xway-deployment.yaml b/kubernetes/deployments/xway-deployment.yaml index 99fed99..ba50fff 100644 --- a/kubernetes/deployments/xway-deployment.yaml +++ b/kubernetes/deployments/xway-deployment.yaml @@ -23,11 +23,11 @@ spec: - containerPort: 5004 resources: {} env: - - name: SCALING + - name: DSE2021_SCALING valueFrom: configMapKeyRef: - name: scaling-configmap - key: scaling + name: simulation-parameters-configmap + key: DSE2021_SCALING restartPolicy: Always serviceAccountName: "" volumes: null diff --git a/project_plan/arch proposal.drawio b/project_plan/arch proposal.drawio index 440f925..9d8399a 100644 --- a/project_plan/arch proposal.drawio +++ b/project_plan/arch proposal.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/project_plan/arch proposal.png b/project_plan/arch proposal.png deleted file mode 100644 index 406c9b8..0000000 Binary files a/project_plan/arch proposal.png and /dev/null differ