diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f79121b8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.sh text eol=lf +*.conf text eol=lf +*.bash text eol=lf +*.yml text eol=lf \ No newline at end of file diff --git a/README.md b/README.md index 5cab8a5e..fe622d82 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ The platform uses the project **OperatorFabric** for notification management. ### Prerequisites - [Git (version 2.40.1)](https://git-scm.com/) -- [Docker (version 24.0.2)](https://www.docker.com/) -- [Docker Compose (version 1.25.0 or later)](https://www.docker.com/) +- [Docker Engine (version 27)](https://www.docker.com/) +- [Docker Compose V2](https://www.docker.com/) ### Setting Up the Environment @@ -63,7 +63,7 @@ Below are the steps to start all services. For other methods, please consult the ### Running All Services (Dev Mode) -1. Set-up environement variables +1. **Set-up environement variables** `VITE_POWERGRID_SIMU`, `VITE_RAILWAY_SIMU` , `VITE_ATM_SIMU` are the simulators' endpoints. @@ -79,31 +79,34 @@ export VITE_ATM_SIMU=http://[Service url]:[Service port] > **_NOTE:_** For this step, you should already have a running simulator. If not, you can use the simulator we provided as an example. For this, please follow the tutorial provided in InteractiveAI/usecases_examples/PowerGrid/ then set the VITE_POWERGRID_SIMU variable to http://YOUR_SERVER_ADDRESS:5100/ > > -2. Run InteractiveAI assistant +2. **Run InteractiveAI assistant** ```sh cd config/dev/cab-standalone ./docker-compose.sh ``` > **_NOTE:_** You will see the word cab (Cockpit Assistant Bidirectionnel) on most files in the project. Note that it was the initial project name of InteractiveAI. Might be updated later. -3. Setting up Keycloak `Frontend URL` - * **Access Keycloak Interface**: +3. **Setting up Keycloak `Frontend URL`** + * Access Keycloak Interface: - Ensure that your Keycloak instance is running and accessible. - Open a web browser and navigate to the Keycloak admin console, typically available at `http://localhost:89/auth/admin`. - * **Login to Keycloak Admin Console**: + * Login to Keycloak Admin Console: - Log in to the Keycloak admin console using your administrator credentials (`admin:admin` by default) - * **Navigate to Client Settings**: + * Configure frontendUrl: + - On the Keycloak admin console, locate and click on the "Realm Settings" section. + - In the Frontend URL setting, add the URL of your Assistant Platform frontend as a valid redirect URI. This URL is typically where your frontend application is hosted. For example, if your frontend is hosted locally for development purposes, you might add `http://localhost:3200/*`. + - After adding the frontend URL, save the changes to update the client settings. + * Configure Valid Redirect URIs: - On the Keycloak admin console, locate and click on the "Clients" section. - Select the client representing your Assistant Platform application. - * **Configure FrontendUrl**: - Within the client settings, look for the "Valid Redirect URIs" or similar configuration field. - - Add the URL of your Assistant Platform frontend as a valid redirect URI. This URL is typically where your frontend application is hosted. For example, if your frontend is hosted locally for development purposes, you might add `http://localhost:3200/*`. - - Ensure that the frontend URL you specify matches the actual URL where your frontend application is accessible. - * **Save Changes**: - - After adding the frontend URL, save the changes to update the client settings. + - Add the URL of your Assistant Platform frontend, it should match the one used in the frontendUrl setting. + - After adding the Valid Redirect URIs, save the changes to update the client settings. + + +4. **Load resources** -4. Load resources -**WARINING:** You need to restart the frontend after updating the URL on keycloak do it before loading the resources. +**WARNING:** You need to restart the frontend after updating the URL on keycloak do it before loading the resources. ```sh docker restart frontend ``` diff --git a/backend/context-service/resources/ATM/schemas.py b/backend/context-service/resources/ATM/schemas.py index 1dfe9489..c065325b 100644 --- a/backend/context-service/resources/ATM/schemas.py +++ b/backend/context-service/resources/ATM/schemas.py @@ -2,10 +2,35 @@ from api.schemas import MetadataSchema from apiflask.fields import Dict, String, Float, List, Integer -class MetadataSchemaATM(MetadataSchema): +from apiflask import Schema, fields +from marshmallow import pre_load +class PlaneMetadataSchemaATM(MetadataSchema): + id_plane = String() ApDest = Dict() Current_airspeed = Float() Latitude = Float() Longitude = Float() wpList = List(Dict()) - id_plane = Integer() + +class MetadataSchemaATM(MetadataSchema): + airplanes = List(fields.Nested(PlaneMetadataSchemaATM), required=True) + + # Backward compatibility: optional fields for the single airplane case + ApDest = Dict(required=False) + Current_airspeed = Float(required=False) + Latitude = Float(required=False) + Longitude = Float(required=False) + wpList = List(Dict(), required=False) + + @pre_load + def handle_backward_compatibility(self, data, **kwargs): + # If the new 'airplanes' field is not provided, assume the old format. + if 'airplanes' not in data: + airplane = {} + for field in ['ApDest', 'Current_airspeed', 'Latitude', 'Longitude', 'wpList']: + if field in data: + airplane[field] = data[field] + # Provide a default id_plane if not present. + airplane.setdefault('id_plane', "X") + data['airplanes'] = [airplane] + return data \ No newline at end of file diff --git a/backend/recommendation-service/resources/PowerGrid/PowerGridgrid2op_poc_simulator/assistant_manager.py b/backend/recommendation-service/resources/PowerGrid/PowerGridgrid2op_poc_simulator/assistant_manager.py index e9491219..401710a2 100644 --- a/backend/recommendation-service/resources/PowerGrid/PowerGridgrid2op_poc_simulator/assistant_manager.py +++ b/backend/recommendation-service/resources/PowerGrid/PowerGridgrid2op_poc_simulator/assistant_manager.py @@ -196,7 +196,7 @@ def get_parade_info(self, act): "Redispatch" # pour renvoyer le kpi type_of_the_reco ) title.append( - "Parade injection: redispatch de source de production" + "Injection recommendation: production source redispatch" ) cpt = 0 for gen_idx in range(act.n_gen): @@ -213,9 +213,9 @@ def get_parade_info(self, act): # storage if act._modif_storage: kpis["type_of_the_reco"] = ( - "Stockage" # pour renvoyer le kpi type_of_the_reco + "Storage" # pour renvoyer le kpi type_of_the_reco ) - title.append("Parade stockage") + title.append("Storage recommendation") cpt = 0 for stor_idx in range(act.n_storage): amount_ = act._storage_power[stor_idx] @@ -225,8 +225,8 @@ def get_parade_info(self, act): description.append(", ") cpt = 1 description.append( - f'Demande à l\'unité "{name_}" de ' - f'{"charger" if amount_ > 0.0 else "decharger"} ' + f'Ask unit "{name_}" to ' + f'{"charge" if amount_ > 0.0 else "discharge"} ' f'{abs(amount_):.2f} MW (setpoint: {amount_:.2f} MW)' ) @@ -235,7 +235,7 @@ def get_parade_info(self, act): kpis["type_of_the_reco"] = ( "Injection" # pour renvoyer le kpi type_of_the_reco ) - title.append("Parade injection") + title.append("Injection recommendation") cpt = 0 for gen_idx in range(act.n_gen): amount_ = act._curtail[gen_idx] @@ -245,8 +245,8 @@ def get_parade_info(self, act): description.append(", ") cpt = 1 description.append( - f'Limiter l\'unité "{name_}" à ' - f'{100.0 * amount_:.1f}% de sa capacité max ' + f'Limit unit "{name_}" to ' + f'{100.0 * amount_:.1f}% of its maximum capacity ' f'(setpoint: {amount_:.3f})' ) @@ -254,22 +254,22 @@ def get_parade_info(self, act): force_line_impact = impact["force_line"] if force_line_impact["changed"]: kpis["type_of_the_reco"] = ( - "Topologique" # pour renvoyer le kpi type_of_the_reco + "Topological" # pour renvoyer le kpi type_of_the_reco ) title.append( - "Parade topologique: connection/deconnection de ligne" + "Topological recommendation: connection/disconnection of line" ) reconnections = force_line_impact["reconnections"] if reconnections["count"] > 0: description.append( - f"Reconnection de {reconnections['count']} lignes " + f"Reconnection of {reconnections['count']} lines " f"({reconnections['powerlines']})" ) disconnections = force_line_impact["disconnections"] if disconnections["count"] > 0: description.append( - f"Déconnection de {disconnections['count']} lignes " + f"Disconnection of {disconnections['count']} lines " f"({disconnections['powerlines']})" ) @@ -277,11 +277,11 @@ def get_parade_info(self, act): swith_line_impact = impact["switch_line"] if swith_line_impact["changed"]: kpis["type_of_the_reco"] = ( - "Topologique" # pour renvoyer le kpi type_of_the_reco + "Topological" # pour renvoyer le kpi type_of_the_reco ) - title.append("Parade topologique: changer l'état d'une ligne") + title.append("Topological: change a line state") description.append( - f"Changer le statut de {swith_line_impact['count']} lignes " + f"Change the state of {swith_line_impact['count']} lines " f"({swith_line_impact['powerlines']})" ) @@ -289,27 +289,27 @@ def get_parade_info(self, act): bus_switch_impact = impact["topology"]["bus_switch"] if len(bus_switch_impact) > 0: kpis["type_of_the_reco"] = ( - "Topologique" # pour renvoyer le kpi type_of_the_reco + "Topological" # pour renvoyer le kpi type_of_the_reco ) title.append( - "Parade topologique: prise de schéma au poste " + "Topological recommendation: Schematic acquisition at substation " + str(bus_switch_impact["substation"]) ) - description.append("Changement de bus:") + description.append("Busbar change:") for switch in bus_switch_impact: description.append( - f"\t \t - Switch bus de {switch['object_type']} id " - f"{switch['object_id']} [au poste {switch['substation']}]" + f"\t \t - Switch bus of {switch['object_type']} id " + f"{switch['object_id']} [at station {switch['substation']}]" ) assigned_bus_impact = impact["topology"]["assigned_bus"] disconnect_bus_impact = impact["topology"]["disconnect_bus"] if len(assigned_bus_impact) > 0 or len(disconnect_bus_impact) > 0: kpis["type_of_the_reco"] = ( - "Topologique" # pour renvoyer le kpi type_of_the_reco + "Topological" # pour renvoyer le kpi type_of_the_reco ) title.append( - "Parade topologique: prise de schéma au poste " + "Topological recommendation: Schematic acquisition at substation " + str(assigned_bus_impact[0]["substation"]) ) if assigned_bus_impact: @@ -320,7 +320,7 @@ def get_parade_info(self, act): description.append(", ") cpt = 1 description.append( - f" Assigner le bus {assigned['bus']} à " + f" Assign bus {assigned['bus']} to " f"{assigned['object_type']} id {assigned['object_id']}" ) if disconnect_bus_impact: @@ -331,8 +331,8 @@ def get_parade_info(self, act): description.append(", ") cpt = 1 description.append( - f"Déconnecter {disconnected['object_type']} avec l'id " - f"{disconnected['object_id']} [au niveau du poste " + f"Disconnect {disconnected['object_type']} with id " + f"{disconnected['object_id']} [at the substation level " f"{disconnected['substation']}]" ) @@ -340,11 +340,11 @@ def get_parade_info(self, act): # then the recommendation is most likely "Do nothing" if not title and act == self.action_do_nothing: kpis["type_of_the_reco"] = ( - "Ne rien faire" # pour renvoyer le kpi type_of_the_reco + "Do nothing" # pour renvoyer le kpi type_of_the_reco ) title.append("Poursuivre") description.append( - "Poursuite du scénario sans intervention extérieur" + "Continuation of the scenario without operator action" ) title = "".join(title) diff --git a/backend/recommendation-service/resources/Railway/manager.py b/backend/recommendation-service/resources/Railway/manager.py index 85b0a9b7..881de5c0 100644 --- a/backend/recommendation-service/resources/Railway/manager.py +++ b/backend/recommendation-service/resources/Railway/manager.py @@ -1,4 +1,4 @@ -# backend/recommendation-service/resources/RTE/manager.py +# backend/recommendation-service/resources/Railway/manager.py from api.manager.base_manager import BaseRecommendationManager diff --git a/config/dev/cab-standalone/docker-compose.sh b/config/dev/cab-standalone/docker-compose.sh index 281eeeec..bac36288 100755 --- a/config/dev/cab-standalone/docker-compose.sh +++ b/config/dev/cab-standalone/docker-compose.sh @@ -44,4 +44,4 @@ fi echo "HOST_IP=${HOST_IP}" >> .env cat .env -docker-compose up -d +docker compose up -d diff --git a/config/dev/cab-standalone/nginx-cors-permissive.conf b/config/dev/cab-standalone/nginx-cors-permissive.conf index 3a6979ac..87c4e5f5 100644 --- a/config/dev/cab-standalone/nginx-cors-permissive.conf +++ b/config/dev/cab-standalone/nginx-cors-permissive.conf @@ -1,4 +1,4 @@ -# docker-compose DNS used to resolved keycloak services +# docker compose DNS used to resolved keycloak services resolver 127.0.0.11 ipv6=off; server { listen 80; diff --git a/config/dev/cab-standalone/nginx-kubernetes.conf b/config/dev/cab-standalone/nginx-kubernetes.conf index 0a47b524..3a331ce7 100644 --- a/config/dev/cab-standalone/nginx-kubernetes.conf +++ b/config/dev/cab-standalone/nginx-kubernetes.conf @@ -1,4 +1,4 @@ -# docker-compose DNS used to resolved users service +# docker compose DNS used to resolved users service # resolver 127.0.0.11 ipv6=off; # Log format to have msec in time + request processing time diff --git a/config/dev/cab-standalone/nginx.conf b/config/dev/cab-standalone/nginx.conf index bf718b7e..2631ed6b 100644 --- a/config/dev/cab-standalone/nginx.conf +++ b/config/dev/cab-standalone/nginx.conf @@ -1,4 +1,4 @@ -# docker-compose DNS used to resolved users service +# docker compose DNS used to resolved users service resolver 127.0.0.11 ipv6=off; # Log format to have msec in time + request processing time diff --git a/config/dev/cab-standalone/stopOpfab.sh b/config/dev/cab-standalone/stopOpfab.sh index 354ca1f4..43ca918a 100755 --- a/config/dev/cab-standalone/stopOpfab.sh +++ b/config/dev/cab-standalone/stopOpfab.sh @@ -1,3 +1,3 @@ #!/bin/bash -docker-compose down -v \ No newline at end of file +docker compose down -v \ No newline at end of file diff --git a/config/dev/recommendation-service/docker-compose.bash b/config/dev/recommendation-service/docker-compose.bash index 1a4adccf..72e6e5db 100755 --- a/config/dev/recommendation-service/docker-compose.bash +++ b/config/dev/recommendation-service/docker-compose.bash @@ -25,4 +25,4 @@ fi echo "HOST_IP=${HOST_IP}" > .env -docker-compose -f "docker-compose-recommendation-service.yml" up --build +docker compose -f "docker-compose-recommendation-service.yml" up --build diff --git a/config/dev/recommendation-service/nginx.conf b/config/dev/recommendation-service/nginx.conf index 5cbc5a0c..b9ad9014 100644 --- a/config/dev/recommendation-service/nginx.conf +++ b/config/dev/recommendation-service/nginx.conf @@ -1,4 +1,4 @@ -# docker-compose DNS used to resolved keycloak services +# docker compose DNS used to resolved keycloak services resolver 127.0.0.11 ipv6=off; server { listen 80; diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 142a6d68..c3c82cea 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -3,6 +3,39 @@ Are you having issues with setting up your environment? Here are some tips that might help. +## Ports already in use + +InteractiveAI uses about twenty ports on server. If a port needed for InteractiveAI is already in use, InteractiveAI will fail (with messages in logs but the can be missed). +The script `resources/checkPorts.sh` tests the availability of each port specified in `config/dev/cab-standalone/docker-compose.yml` and write a diagnosis on console. + +It it succeeds: +``` +brettevi@PCAlien:~/Projets/InteractiveAI$ ./resources/checkPorts.sh +Ports used for InteractiveAI: 89 3200 5000 5100 5200 5400 5433 5434 5436 5437 5438 5500 12002 12100 12102 12103 12104 27017 +All is fine: all ports used by InteractiveAI are available +``` + +If it fails: +``` +brettevi@PCAlien:~/Projets/InteractiveAI$ ./resources/checkPorts.sh +Ports used for InteractiveAI: 89 3200 5000 5100 5200 5400 5433 5434 5436 5437 5438 5500 12002 12100 12102 12103 12104 27017 +docker: Error response from daemon: driver failed programming external connectivity on endpoint stoic_williams (b69f8285b2ec63145267ad7ea04969cf58dc423528729c7c3f07b5d5c3ccc342): Bind for 0.0.0.0:89 failed: port is already allocated. +... +docker: Error response from daemon: driver failed programming external connectivity on endpoint zen_feistel (f3047f6ae4ebf5e78dc034137d24a832e2ff60490424ba8ba4ae2531d2222142): Bind for 0.0.0.0:12104 failed: port is already allocated. +docker: Error response from daemon: driver failed programming external connectivity on endpoint keen_swanson (82f2ac17d11057767379dc0f492b5a9b8fd7620fb562e8107e9ce1453e548052): Bind for 0.0.0.0:27017 failed: port is already allocated. +Check your counfiguration: 18 port(s) used by InteractiveAI are already used +InteractiveAI can't run on this platform with this /home/brettevi/Projets/InteractiveAI/config/dev/cab-standalone/docker-compose.yml ports configuration +``` + +This command may also be used with a specific docker-compose.yml file. For example, to test Powergrid simulator ports availability, one can use: +``` +brettevi@PCAlien:~/Projets/InteractiveAI$ ./resources/checkPorts.sh usecases_examples/PowerGrid/docker-compose.yml +Ports used for InteractiveAI: 5150 +docker: Error response from daemon: driver failed programming external connectivity on endpoint funny_rhodes (6291f43617a7798a833fdeee05c32c75c2d0bf765eac5dc3b8fe08b7255e57a1): Bind for 0.0.0.0:5150 failed: port is already allocated. +Check your counfiguration: 1 port(s) used by InteractiveAI are already used +InteractiveAI can't run on this platform with this usecases_examples/PowerGrid/docker-compose.yml ports configuration +``` + ## EoL Sequence Configuration errors. Some users may encounter issues if their system is automatically converting end of line sequence from LF to CRLF. @@ -18,6 +51,6 @@ The .env should contain: HOST_IP= ``` -If the IP_Address is not your network IP address, please set it manually and run the system using native docker-compose commands. +If the IP_Address is not your network IP address, please set it manually and run the system using native docker compose commands. > **_NOTE:_** You are welcome to contribute with any issue that you encounter during setup. \ No newline at end of file diff --git a/frontend/public/img/icons/map_markers/DA.svg b/frontend/public/img/icons/map_markers/ATM.svg similarity index 100% rename from frontend/public/img/icons/map_markers/DA.svg rename to frontend/public/img/icons/map_markers/ATM.svg diff --git a/frontend/public/img/icons/map_markers/SNCF.svg b/frontend/public/img/icons/map_markers/Railway.svg similarity index 100% rename from frontend/public/img/icons/map_markers/SNCF.svg rename to frontend/public/img/icons/map_markers/Railway.svg diff --git a/frontend/src/components/organisms/Map.vue b/frontend/src/components/organisms/Map.vue index 81e4841d..be58466b 100644 --- a/frontend/src/components/organisms/Map.vue +++ b/frontend/src/components/organisms/Map.vue @@ -83,11 +83,12 @@ import { useMapStore } from '@/stores/components/map' import type { Waypoint } from '@/types/components/map' import { criticalityToColor, maxCriticality } from '@/utils/utils' -withDefaults( +const props = withDefaults( defineProps<{ tileLayers?: string[] contextClick?: (waypoint: Waypoint) => void waypointClick?: (waypoint: Waypoint) => void + autoFit?: boolean }>(), { tileLayers: () => ['http://{s}.tile.osm.org/{z}/{x}/{y}.png'], @@ -103,9 +104,17 @@ const lockView = ref(true) const zoom = ref(6) const map = ref() -watch(mapStore.contextWaypoints, () => { - toggleLockView() -}) +watch( + () => mapStore.contextWaypoints, + () => { + if (props.autoFit) { + toggleLockView() + } + +} +) + + watch(appStore.panels, () => { map.value.leafletObject.invalidateSize() }) diff --git a/frontend/src/entities/ATM/CAB/Context.vue b/frontend/src/entities/ATM/CAB/Context.vue index b030d08a..14b20887 100644 --- a/frontend/src/entities/ATM/CAB/Context.vue +++ b/frontend/src/entities/ATM/CAB/Context.vue @@ -3,18 +3,17 @@ +