Compare commits
23 Commits
v1.3.0-alp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e054db5ca4 | ||
|
|
c1706f337d | ||
|
|
c301692a98 | ||
|
|
43b8c3e008 | ||
|
|
f11a57d318 | ||
|
|
dcb2889824 | ||
|
|
5c76add30b | ||
|
|
6f329184e2 | ||
|
|
8510f680da | ||
|
|
051fc3cb22 | ||
|
|
9dc297d24b | ||
|
|
9930419c3c | ||
|
|
7ba5bdc604 | ||
|
|
3b882df2e1 | ||
|
|
b06dbdbba9 | ||
|
|
438f9aa952 | ||
|
|
2e1a8a768d | ||
|
|
a3a066034d | ||
|
|
d0fbcd1e2d | ||
|
|
a70f5adf15 | ||
|
|
85ddea41e4 | ||
|
|
d0c0cc8b0e | ||
|
|
e20790dce4 |
2
.env.citrineos-ui
Normal file
2
.env.citrineos-ui
Normal file
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_AUTH_PROVIDER=***
|
||||
N...**
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ tools/*/__pycache__
|
||||
tools/*/.git
|
||||
*.zip
|
||||
tools/citrineos-core-main/src
|
||||
tools/citrineos-core-main/.pnpm-store/
|
||||
|
||||
158
config/docker-compose-citrineos-everest.yml
Normal file
158
config/docker-compose-citrineos-everest.yml
Normal file
@@ -0,0 +1,158 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
citrineos-server:
|
||||
image: ghcr.io/citrineos/citrineos-server:latest
|
||||
container_name: cariflex-citrineos-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_NAME: "all"
|
||||
APP_ENV: "docker"
|
||||
AWS_REGION: us-east-1
|
||||
AWS_ACCESS_KEY_ID: minioadmin
|
||||
AWS_SECRET_ACCESS_KEY: minioadmin
|
||||
DB_STRATEGY: "migrate"
|
||||
BOOTSTRAP_CITRINEOS_DATABASE_HOST: "cariflex-citrineos-db"
|
||||
BOOTSTRAP_CITRINEOS_CONFIG_FILENAME: "config.json"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE: "local"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_FILE_PATH: "/data"
|
||||
CONFIG_CITRINEOS_WIPE_FILE_ON_START: "true"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
cariflex-amqp:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- citrineos-data:/data
|
||||
ports:
|
||||
- "8081:8080"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-citrineos-db:
|
||||
image: postgis/postgis:16-3.5
|
||||
container_name: cariflex-citrineos-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: citrine
|
||||
POSTGRES_USER: citrine
|
||||
POSTGRES_PASSWORD: citrine
|
||||
volumes:
|
||||
- citrineos-db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready --username=citrine
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-amqp:
|
||||
image: rabbitmq:3-management
|
||||
container_name: cariflex-amqp
|
||||
networks:
|
||||
cariflex-internal:
|
||||
aliases:
|
||||
- amqp-broker
|
||||
traefik-public:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.rabbitmq.rule=Host(`amqp.digitribe.fr`)"
|
||||
- "traefik.http.routers.rabbitmq.entrypoints=websecure"
|
||||
- "traefik.http.routers.rabbitmq.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.rabbitmq.loadbalancer.server.port=15672"
|
||||
volumes:
|
||||
- citrineos-amqp-data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: rabbitmq-diagnostics -q ping
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
hasura:
|
||||
image: hasura/graphql-engine:v2.40.0
|
||||
container_name: cariflex-hasura
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:8080"
|
||||
environment:
|
||||
HASURA_GRAPHQL_DATABASE_URL: "postgresql://citrine:***@cariflex-citrineos-db:5432/citrine"
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: "Digitribe972"
|
||||
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "anonymous"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.hasura.rule=Host(`hasura.digitribe.fr`)"
|
||||
- "traefik.http.routers.hasura.entrypoints=websecure"
|
||||
- "traefik.http.routers.hasura.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.hasura.loadbalancer.server.port=8080"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
citrineos-operator-ui:
|
||||
image: citrineos-core-main-citrine-ui:latest
|
||||
container_name: cariflex-citrineos-operator-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3002:3000"
|
||||
environment:
|
||||
NEXTAUTH_SECRET: Digitribe972
|
||||
ADMIN_PASSWORD: Digitribe972
|
||||
depends_on:
|
||||
- hasura
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.citrineos-ui.rule=Host(`citrineos.digitribe.fr`)"
|
||||
- "traefik.http.routers.citrineos-ui.entrypoints=websecure"
|
||||
- "traefik.http.routers.citrineos-ui.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.citrineos-ui.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
# === EVerest (simulateur de charge OCPP 2.0.1) ===
|
||||
everest-mqtt:
|
||||
image: ghcr.io/everest/everest-demo/mqtt-server:0.0.16
|
||||
container_name: cariflex-everest-mqtt
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
everest-nodered:
|
||||
image: ghcr.io/everest/everest-demo/nodered:0.0.16
|
||||
container_name: cariflex-everest-nodered
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- everest-mqtt
|
||||
environment:
|
||||
- MQTT_SERVER_ADDRESS=everest-mqtt
|
||||
- FLOWS=/config/config-sil-two-evse-flow.json
|
||||
networks:
|
||||
- cariflex-internal
|
||||
ports:
|
||||
- "1880:1880"
|
||||
|
||||
volumes:
|
||||
citrineos-data:
|
||||
driver: local
|
||||
citrineos-db-data:
|
||||
driver: local
|
||||
citrineos-amqp-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
cariflex-internal:
|
||||
name: config_cariflex-internal
|
||||
external: true
|
||||
@@ -120,6 +120,41 @@ services:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
# === EVerest MQTT + NodeRED (UI de contrôle) ===
|
||||
everest-mqtt:
|
||||
image: ghcr.io/everest/everest-demo/mqtt-server:0.0.16
|
||||
container_name: cariflex-everest-mqtt
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
everest-nodered:
|
||||
image: ghcr.io/everest/everest-demo/nodered:0.0.16
|
||||
container_name: cariflex-everest-nodered
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- everest-mqtt
|
||||
environment:
|
||||
- MQTT_SERVER_ADDRESS=everest-mqtt
|
||||
- FLOWS=/config/config-sil-two-evse-flow.json
|
||||
networks:
|
||||
- cariflex-internal
|
||||
ports:
|
||||
- "1880:1880"
|
||||
|
||||
# === OCPP 2.0.1 Simulators ===
|
||||
ocpp-simulator:
|
||||
build:
|
||||
context: /home/eric/cariflex/scripts
|
||||
dockerfile: Dockerfile.simulator
|
||||
container_name: cariflex-ocpp-simulator
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
OCPP_HOST: "cariflex-citrineos-server"
|
||||
OCPP_PORT: "8082"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
volumes:
|
||||
citrineos-data:
|
||||
driver: local
|
||||
|
||||
@@ -142,6 +142,16 @@
|
||||
| `/home/eric/flexmeasures/docker-compose.yml` | FM core stack |
|
||||
| `/home/eric/flexmeasures/docker-compose.openadr.yml` | OpenADR VTN/VEN |
|
||||
| `/home/eric/cariflex/config/docker-compose-citrineos.yml` | CitrineOS stack |
|
||||
| `/home/eric/cariflex/config/docker-compose-citrineos-ui-noproxy.yml` | UI without Traefik (testing) |
|
||||
|
||||
### CitrineOS Operator UI
|
||||
- **URL**: https://citrineos.digitribe.fr
|
||||
- **Image**: `citrineos-core-main-citrine-ui:latest` (custom build)
|
||||
- **Build**: `cd /home/eric/cariflex/tools/citrineos-core-main && docker compose -f docker-compose.local.yml build citrine-ui`
|
||||
- **Tech stack**: Next.js 15.2, React 19, Refine, Leaflet + OpenStreetMap
|
||||
- **Map**: Replaced Google Maps with Leaflet + OpenStreetMap (no API key needed)
|
||||
- **Geocoding**: Nominatim OSM (free, no API key)
|
||||
- **Dependencies**: `leaflet`, `react-leaflet`, `@types/leaflet`
|
||||
|
||||
### Deployment Commands
|
||||
```bash
|
||||
@@ -154,6 +164,9 @@ cd /home/eric/flexmeasures && docker compose -f docker-compose.openadr.yml up -d
|
||||
# CitrineOS stack
|
||||
cd /home/eric/cariflex/config && docker compose -f docker-compose-citrineos.yml up -d
|
||||
|
||||
# Rebuild UI (after code changes)
|
||||
cd /home/eric/cariflex/tools/citrineos-core-main && docker compose -f docker-compose.local.yml build citrine-ui && docker compose -f docker-compose-citrineos.yml up -d citrineos-operator-ui
|
||||
|
||||
# Verify all
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}" | grep -E "flexmeasures|openadr|citrine|grafana"
|
||||
```
|
||||
|
||||
BIN
docs/cariflex-backup-20260613-100412.tar.gz
Normal file
BIN
docs/cariflex-backup-20260613-100412.tar.gz
Normal file
Binary file not shown.
3076
docs/hasura-metadata-backup-20260613.json
Normal file
3076
docs/hasura-metadata-backup-20260613.json
Normal file
File diff suppressed because it is too large
Load Diff
3076
docs/hasura-metadata-final-20260613.json
Normal file
3076
docs/hasura-metadata-final-20260613.json
Normal file
File diff suppressed because it is too large
Load Diff
3076
docs/hasura-metadata-final-20260614.json
Normal file
3076
docs/hasura-metadata-final-20260614.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/session-backup-20260613-121410.tar.gz
Normal file
BIN
docs/session-backup-20260613-121410.tar.gz
Normal file
Binary file not shown.
BIN
docs/session-backup-20260613-121455.tar.gz
Normal file
BIN
docs/session-backup-20260613-121455.tar.gz
Normal file
Binary file not shown.
5
scripts/Dockerfile.simulator
Normal file
5
scripts/Dockerfile.simulator
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY ocpp-simulator.js .
|
||||
COPY ocpp-simulator-multi.js .
|
||||
CMD ["node", "ocpp-simulator-multi.js"]
|
||||
5
scripts/Dockerfile.sp0
Normal file
5
scripts/Dockerfile.sp0
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY ocpp-simulator-multi.js .
|
||||
COPY ocpp-sp0-connector.js .
|
||||
CMD ["node", "ocpp-sp0-connector.js"]
|
||||
29
scripts/configure-auth.py
Normal file
29
scripts/configure-auth.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Configure BasicAuthPassword for all 15 Cariflex charging stations"""
|
||||
import json, time, urllib.request, urllib.error
|
||||
|
||||
CITRINEOS_URL = "http://localhost:8081"
|
||||
PASSWORD = "DEADBEEFDEADBEEF"
|
||||
|
||||
for i in range(1, 16):
|
||||
cp_id = f"CP{i:03d}"
|
||||
url = f"{CITRINEOS_URL}/data/monitoring/variableAttribute?stationId={cp_id}&setOnCharger=true"
|
||||
payload = json.dumps({
|
||||
"component": {"name": "SecurityCtrlr"},
|
||||
"variable": {"name": "BasicAuthPassword"},
|
||||
"variableAttribute": [{"value": PASSWORD}],
|
||||
"variableCharacteristics": {"dataType": "passwordString", "supportsMonitoring": False}
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(url, data=payload, method='PUT',
|
||||
headers={'Content-Type': 'application/json'})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
print(f"OK {cp_id}: HTTP {resp.status}")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"FAIL {cp_id}: HTTP {e.code}")
|
||||
except Exception as e:
|
||||
print(f"FAIL {cp_id}: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
print("Done")
|
||||
33
scripts/configure-auth.sh
Normal file
33
scripts/configure-auth.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# Configure BasicAuthPassword for all 15 Cariflex charging stations
|
||||
# This allows OCPP 2.0.1 simulators to connect via Security Profile 1
|
||||
|
||||
set -e
|
||||
|
||||
CitRINEOS_URL="http://localhost:8081"
|
||||
PASSWORD="DEADBEEFDEADBEEF"
|
||||
|
||||
echo "=== Configuring BasicAuthPassword for all stations ==="
|
||||
|
||||
for i in $(seq 1 15); do
|
||||
# Format: CP001, CP002, ... CP015
|
||||
CP_ID=$(printf "CP%03d" $i)
|
||||
|
||||
response=$(curl -s -o /dev/null -w "%{http_code}" --location --request PUT \
|
||||
"${CitRINEOS_URL}/data/monitoring/variableAttribute?stationId=${CP_ID}&setOnCharger=true" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data-raw "{
|
||||
\"component\": { \"name\": \"SecurityCtrlr\" },
|
||||
\"variable\": { \"name\": \"BasicAuthPassword\" },
|
||||
\"variableAttribute\": [{ \"value\": \"${PASSWORD}\" }],
|
||||
\"variableCharacteristics\": { \"dataType\": \"passwordString\", \"supportsMonitoring\": false }
|
||||
}")
|
||||
|
||||
if [ "$response" -ge 200 ] && [ "$response" -lt 300 ]; then
|
||||
echo "✅ ${CP_ID}: BasicAuthPassword configured"
|
||||
else
|
||||
echo "❌ ${CP_ID}: Failed (HTTP ${response})"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== Done ==="
|
||||
30
scripts/configure-ocpi-everest.sh
Normal file
30
scripts/configure-ocpi-everest.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# === Post-deploy OCPI + Everest configuration for Cariflex ===
|
||||
# Run this AFTER docker compose up succeeds
|
||||
|
||||
set -e
|
||||
|
||||
CSMS_URL="http://localhost:8081"
|
||||
GRAPHQL_URL="http://localhost:8090/v1/graphql"
|
||||
DB_CONTAINER="cariflex-citrineos-db"
|
||||
|
||||
echo "=== Step 1: Seed OCPI database ==="
|
||||
# Run inside the OCPI container to seed default tenant + emsp
|
||||
docker exec cariflex-ocpi npm run seed-db 2>&1 || echo "Seed may have run before, continuing..."
|
||||
|
||||
echo "=== Step 2: Verify Citiveness ==="
|
||||
# Check CitrineOS Core is healthy
|
||||
curl -sf "${CSMS_URL}/ocpp/health" && echo "CitrineOS Core: OK" || echo "CitrineOS Core: UNHEALTHY"
|
||||
|
||||
echo "=== Step 3: Configure EVerest Manager OCPP target ==="
|
||||
# The manager already points to ws://cariflex-citrineos-server:8080/cp001 via Dockerfile
|
||||
# Check manager is running
|
||||
docker ps --filter "name=cariflex-everest-manager" --format "{{.Status}}" && echo "Everest Manager: running" || echo "Everest Manager: NOT RUNNING"
|
||||
|
||||
echo "=== Step 4: Verify UI connectivity ==="
|
||||
echo " Everest NodeRED UI: http://localhost:1880/ui/"
|
||||
echo " Everest OCPP Logs: http://localhost:8888"
|
||||
echo " CitrineOS UI: https://citrineos.digitribe.fr"
|
||||
echo " Hasura Console: https://hasura.digitribe.fr"
|
||||
echo ""
|
||||
echo "=== DONE ==="
|
||||
30
scripts/everest-cariflex-entrypoint.sh
Normal file
30
scripts/everest-cariflex-entrypoint.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Entrypoint personnalisé pour Everest Manager - Cariflex
|
||||
# Configure l'OCPP CSMS URL puis lance le simulateur
|
||||
|
||||
set -e
|
||||
|
||||
CSMS_URL="${EVEREST_TARGET_URL:-ws://cariflex-citrineos-server:8080/cp001}"
|
||||
echo "=== EVerest Manager - Cariflex ==="
|
||||
echo "CSMS URL: $CSMS_URL"
|
||||
|
||||
# Installer sqlite3 si nécessaire
|
||||
apk add --no-cache sqlite3 2>/dev/null || true
|
||||
|
||||
# Configurer l'OCPP CSMS URL dans la base de données
|
||||
DB_PATH="/ext/source/build/dist/share/everest/modules/OCPP201/device_model_storage.db"
|
||||
if [ -f "$DB_PATH" ]; then
|
||||
echo "Configuring OCPP CSMS URL in device model DB..."
|
||||
sqlite3 "$DB_PATH" "UPDATE VARIABLE_ATTRIBUTE SET value = '[{\"configurationSlot\": 1, \"connectionData\": {\"messageTimeout\": 30, \"ocppCsmsUrl\": \"$CSMS_URL\", \"ocppInterface\": \"Wired0\", \"ocppTransport\": \"JSON\", \"ocppVersion\": \"OCPP20\", \"securityProfile\": 1}}]' WHERE variable_Id IN (SELECT id FROM VARIABLE WHERE name = 'NetworkConnectionProfiles');"
|
||||
echo "OCPP CSMS URL configured."
|
||||
else
|
||||
echo "WARNING: device_model_storage.db not found at $DB_PATH"
|
||||
fi
|
||||
|
||||
# Installer http-server pour les logs
|
||||
npm i -g http-server 2>/dev/null || true
|
||||
|
||||
# Lancer le simulateur OCPP 2.0.1 avec PNC (Plug & Charge)
|
||||
echo "Starting EVerest OCPP 2.0.1 simulator..."
|
||||
chmod +x /ext/source/build/run-scripts/run-sil-ocpp201-pnc.sh
|
||||
exec /ext/source/build/run-scripts/run-sil-ocpp201-pnc.sh
|
||||
241
scripts/fix-hasura-relations.py
Normal file
241
scripts/fix-hasura-relations.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script pour recréer toutes les relations Hasura avec les noms camelCase.
|
||||
Utilise l'API metadata Hasura pour créer les relations manquantes.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ADMIN_SECRET = 'Digitribe972'
|
||||
METADATA_URL = 'http://localhost:8082/v1/metadata'
|
||||
GRAPHQL_URL = 'http://localhost:8082/v1/graphql'
|
||||
|
||||
def graphql_query(query):
|
||||
"""Execute a GraphQL query and return the result."""
|
||||
r = subprocess.run(
|
||||
['curl', '-s', '-X', 'POST', GRAPHQL_URL,
|
||||
'-H', f'x-hasura-admin-secret: {ADMIN_SECRET}',
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-d', json.dumps({"query": query})],
|
||||
capture_output=True, text=True, timeout=15
|
||||
)
|
||||
return json.loads(r.stdout)
|
||||
|
||||
def metadata_command(payload):
|
||||
"""Execute a metadata command and return the result."""
|
||||
r = subprocess.run(
|
||||
['curl', '-s', '-X', 'POST', METADATA_URL,
|
||||
'-H', f'x-hasura-admin-secret: {ADMIN_SECRET}',
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-d', json.dumps(payload)],
|
||||
capture_output=True, text=True, timeout=15
|
||||
)
|
||||
return json.loads(r.stdout)
|
||||
|
||||
def test_relation(table, relation, fields='{ id }'):
|
||||
"""Test if a relation works by querying it."""
|
||||
query = f'{{ {table}(where: {{id: {{_eq: "CP001"}}}}) {{ id {relation} {fields} }} }}'
|
||||
result = graphql_query(query)
|
||||
if 'errors' in result:
|
||||
return False, result['errors'][0]['message'][:100]
|
||||
return True, "OK"
|
||||
|
||||
def create_relation(table, name, rel_type, remote_table, col_map):
|
||||
"""Create a relationship in Hasura."""
|
||||
if rel_type == 'object':
|
||||
payload = {
|
||||
"type": "pg_create_object_relationship",
|
||||
"args": {
|
||||
"source": "default",
|
||||
"table": {"schema": "public", "name": table},
|
||||
"name": name,
|
||||
"using": {
|
||||
"manual_configuration": {
|
||||
"remote_table": {"schema": "public", "name": remote_table},
|
||||
"column_mapping": col_map
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"type": "pg_create_array_relationship",
|
||||
"args": {
|
||||
"source": "default",
|
||||
"table": {"schema": "public", "name": table},
|
||||
"name": name,
|
||||
"using": {
|
||||
"manual_configuration": {
|
||||
"remote_table": {"schema": "public", "name": remote_table},
|
||||
"column_mapping": col_map
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = metadata_command(payload)
|
||||
if 'message' in result and result['message'] == 'success':
|
||||
return True
|
||||
elif 'error' in result:
|
||||
if 'already exists' in result['error']:
|
||||
return True # Already exists, that's OK
|
||||
print(f" ERROR: {result['error'][:200]}")
|
||||
return False
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=== Diagnostic des relations Hasura ===\n")
|
||||
|
||||
# Test critical relations
|
||||
critical_relations = [
|
||||
("ChargingStations", "location", "{ id name }"),
|
||||
("ChargingStations", "evses", "{ id evseId }"),
|
||||
("ChargingStations", "connectors", "{ id status }"),
|
||||
("ChargingStations", "transactions", "{ id }"),
|
||||
("ChargingStations", "latestStatusNotifications", "{ statusNotification { connectorStatus } }"),
|
||||
("ChargingStations", "tenant", "{ id name }"),
|
||||
("Evses", "connectors", "{ id status }"),
|
||||
("Transactions", "chargingStation", "{ id }"),
|
||||
("Transactions", "evse", "{ id }"),
|
||||
("Transactions", "connector", "{ id }"),
|
||||
("Transactions", "location", "{ id name }"),
|
||||
("LatestStatusNotifications", "statusNotification", "{ connectorStatus }"),
|
||||
]
|
||||
|
||||
print("Test des relations critiques:")
|
||||
all_ok = True
|
||||
for table, relation, fields in critical_relations:
|
||||
ok, msg = test_relation(table, relation, fields)
|
||||
status = "OK" if ok else "FAIL"
|
||||
print(f" {status}: {table}.{relation}")
|
||||
if not ok:
|
||||
all_ok = False
|
||||
print(f" -> {msg}")
|
||||
|
||||
if all_ok:
|
||||
print("\n=== Toutes les relations fonctionnent ! ===")
|
||||
return
|
||||
|
||||
print("\n=== Recréation des relations manquantes ===\n")
|
||||
|
||||
# Define all relations to create
|
||||
relations = [
|
||||
# ChargingStations
|
||||
("ChargingStations", "location", "object", "Locations", {"locationId": "id"}),
|
||||
("ChargingStations", "tenant", "object", "Tenants", {"tenantId": "id"}),
|
||||
("ChargingStations", "evses", "array", "Evses", {"id": "stationId"}),
|
||||
("ChargingStations", "connectors", "array", "Connectors", {"id": "stationId"}),
|
||||
("ChargingStations", "transactions", "array", "Transactions", {"id": "stationId"}),
|
||||
("ChargingStations", "latestStatusNotifications", "array", "LatestStatusNotifications", {"id": "stationId"}),
|
||||
("ChargingStations", "variableAttributes", "array", "VariableAttributes", {"id": "stationPkId"}),
|
||||
|
||||
# Evses
|
||||
("Evses", "connectors", "array", "Connectors", {"evseId": "evseId"}),
|
||||
("Evses", "transactions", "array", "Transactions", {"evseId": "evseId"}),
|
||||
("Evses", "chargingStation", "object", "ChargingStations", {"stationId": "id"}),
|
||||
|
||||
# Connectors
|
||||
("Connectors", "evse", "object", "Evses", {"evseId": "id"}),
|
||||
("Connectors", "chargingStation", "object", "ChargingStations", {"stationId": "id"}),
|
||||
|
||||
# Transactions
|
||||
("Transactions", "chargingStation", "object", "ChargingStations", {"stationId": "id"}),
|
||||
("Transactions", "evse", "object", "Evses", {"evseId": "id"}),
|
||||
("Transactions", "connector", "object", "Connectors", {"connectorId": "id"}),
|
||||
("Transactions", "authorization", "object", "Authorizations", {"authorizationId": "id"}),
|
||||
("Transactions", "location", "object", "Locations", {"locationId": "id"}),
|
||||
|
||||
# LatestStatusNotifications
|
||||
("LatestStatusNotifications", "statusNotification", "object", "StatusNotifications", {"statusNotificationId": "id"}),
|
||||
("LatestStatusNotifications", "chargingStation", "object", "ChargingStations", {"stationId": "id"}),
|
||||
|
||||
# Locations
|
||||
("Locations", "chargingStations", "array", "ChargingStations", {"locationId": "id"}),
|
||||
|
||||
# StatusNotifications
|
||||
("StatusNotifications", "chargingStation", "object", "ChargingStations", {"stationId": "id"}),
|
||||
("StatusNotifications", "evse", "object", "Evses", {"evseId": "id"}),
|
||||
("StatusNotifications", "connector", "object", "Connectors", {"connectorId": "id"}),
|
||||
|
||||
# Authorizations
|
||||
("Authorizations", "transactions", "array", "Transactions", {"authorizationId": "id"}),
|
||||
("Authorizations", "chargingStation", "object", "ChargingStations", {"stationId": "id"}),
|
||||
|
||||
# Tenants
|
||||
("Tenants", "chargingStations", "array", "ChargingStations", {"tenantId": "id"}),
|
||||
("Tenants", "evses", "array", "Evses", {"tenantId": "id"}),
|
||||
("Tenants", "connectors", "array", "Connectors", {"tenantId": "id"}),
|
||||
("Tenants", "transactions", "array", "Transactions", {"tenantId": "id"}),
|
||||
("Tenants", "locations", "array", "Locations", {"tenantId": "id"}),
|
||||
("Tenants", "authorizations", "array", "Authorizations", {"tenantId": "id"}),
|
||||
("Tenants", "tenantPartners", "array", "TenantPartners", {"tenantId": "id"}),
|
||||
("Tenants", "statusNotifications", "array", "StatusNotifications", {"tenantId": "id"}),
|
||||
("Tenants", "latestStatusNotifications", "array", "LatestStatusNotifications", {"tenantId": "id"}),
|
||||
("Tenants", "variableAttributes", "array", "VariableAttributes", {"tenantId": "id"}),
|
||||
("Tenants", "chargingProfiles", "array", "ChargingProfiles", {"tenantId": "id"}),
|
||||
("Tenants", "chargingSchedules", "array", "ChargingSchedules", {"tenantId": "id"}),
|
||||
("Tenants", "salesTariffs", "array", "SalesTariffs", {"tenantId": "id"}),
|
||||
("Tenants", "messageInfos", "array", "MessageInfos", {"tenantId": "id"}),
|
||||
("Tenants", "securityEvents", "array", "SecurityEvents", {"tenantId": "id"}),
|
||||
("Tenants", "certificates", "array", "Certificates", {"tenantId": "id"}),
|
||||
("Tenants", "boots", "array", "Boots", {"tenantId": "id"}),
|
||||
("Tenants", "asyncJobStatuses", "array", "AsyncJobStatuses", {"tenantId": "id"}),
|
||||
("Tenants", "eventData", "array", "EventData", {"tenantId": "id"}),
|
||||
("Tenants", "subscriptions", "array", "Subscriptions", {"tenantId": "id"}),
|
||||
("Tenants", "reservations", "array", "Reservations", {"tenantId": "id"}),
|
||||
("Tenants", "transactionEvents", "array", "TransactionEvents", {"tenantId": "id"}),
|
||||
("Tenants", "startTransactions", "array", "StartTransactions", {"tenantId": "id"}),
|
||||
("Tenants", "stopTransactions", "array", "StopTransactions", {"tenantId": "id"}),
|
||||
("Tenants", "meterValues", "array", "MeterValues", {"tenantId": "id"}),
|
||||
("Tenants", "chargingStationNetworkProfiles", "array", "ChargingStationNetworkProfiles", {"tenantId": "id"}),
|
||||
("Tenants", "serverNetworkProfiles", "array", "ServerNetworkProfiles", {"tenantId": "id"}),
|
||||
("Tenants", "componentVariables", "array", "ComponentVariables", {"tenantId": "id"}),
|
||||
("Tenants", "components", "array", "Components", {"tenantId": "id"}),
|
||||
("Tenants", "variables", "array", "Variables", {"tenantId": "id"}),
|
||||
("Tenants", "evseTypes", "array", "EvseTypes", {"tenantId": "id"}),
|
||||
("Tenants", "compositeSchedules", "array", "CompositeSchedules", {"tenantId": "id"}),
|
||||
("Tenants", "changeConfigurations", "array", "ChangeConfigurations", {"tenantId": "id"}),
|
||||
("Tenants", "chargingStationSequences", "array", "ChargingStationSequences", {"tenantId": "id"}),
|
||||
("Tenants", "chargingStationSecurityInfos", "array", "ChargingStationSecurityInfos", {"tenantId": "id"}),
|
||||
("Tenants", "localListAuthorizations", "array", "LocalListAuthorizations", {"tenantId": "id"}),
|
||||
("Tenants", "localListVersionAuthorizations", "array", "LocalListVersionAuthorizations", {"tenantId": "id"}),
|
||||
("Tenants", "localListVersions", "array", "LocalListVersions", {"tenantId": "id"}),
|
||||
("Tenants", "sendLocalListAuthorizations", "array", "SendLocalListAuthorizations", {"tenantId": "id"}),
|
||||
("Tenants", "sendLocalLists", "array", "SendLocalLists", {"tenantId": "id"}),
|
||||
("Tenants", "deleteCertificateAttempts", "array", "DeleteCertificateAttempts", {"tenantId": "id"}),
|
||||
("Tenants", "installCertificateAttempts", "array", "InstallCertificateAttempts", {"tenantId": "id"}),
|
||||
("Tenants", "installedCertificates", "array", "InstalledCertificates", {"tenantId": "id"}),
|
||||
]
|
||||
|
||||
created = 0
|
||||
failed = 0
|
||||
|
||||
for table, name, rel_type, remote_table, col_map in relations:
|
||||
print(f"Creating {table}.{name}...", end=" ")
|
||||
if create_relation(table, name, rel_type, remote_table, col_map):
|
||||
print("OK")
|
||||
created += 1
|
||||
else:
|
||||
print("FAIL")
|
||||
failed += 1
|
||||
|
||||
print(f"\n=== Résultat: {created} créées, {failed} échouées ===")
|
||||
|
||||
# Retest critical relations
|
||||
print("\n=== Vérification finale ===\n")
|
||||
all_ok = True
|
||||
for table, relation, fields in critical_relations:
|
||||
ok, msg = test_relation(table, relation, fields)
|
||||
status = "OK" if ok else "FAIL"
|
||||
print(f" {status}: {table}.{relation}")
|
||||
if not ok:
|
||||
all_ok = False
|
||||
|
||||
if all_ok:
|
||||
print("\n🎉 Toutes les relations fonctionnent !")
|
||||
else:
|
||||
print("\n❌ Certaines relations ne fonctionnent pas encore.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
133
scripts/ocpp-simulator-multi.js
Normal file
133
scripts/ocpp-simulator-multi.js
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PASSWORD='DEADBEEFDEADBEEF';
|
||||
const STATIONS = Array.from({length: 15}, (_, i) => ({
|
||||
id: `CP${String(i+1).padStart(3, '0')}`,
|
||||
path: `/1/CP${String(i+1).padStart(3, '0')}`,
|
||||
}));
|
||||
|
||||
const WS_HOST = process.env.OCPP_HOST || 'cariflex-citrineos-server';
|
||||
const WS_PORT = parseInt(process.env.OCPP_PORT || '8082');
|
||||
let msgId = 0;
|
||||
|
||||
function encodeFrame(payload) {
|
||||
const maskKey = crypto.randomBytes(4);
|
||||
const pb = Buffer.from(payload, 'utf8');
|
||||
const len = pb.length;
|
||||
const fl = len < 126 ? 6 + len : 8 + len;
|
||||
const f = Buffer.alloc(fl);
|
||||
f[0] = 0x81;
|
||||
if (len < 126) { f[1] = 0x80 | len; }
|
||||
else { f[1] = 0x80 | 126; f.writeUInt16BE(len, 2); }
|
||||
maskKey.copy(f, fl - len - 4);
|
||||
for (let i = 0; i < len; i++) f[fl - len + i] = pb[i] ^ maskKey[i % 4];
|
||||
return f;
|
||||
}
|
||||
|
||||
function parseFrames(buf) {
|
||||
const msgs = [];
|
||||
while (buf.length >= 2) {
|
||||
const op = buf[0] & 0x0F;
|
||||
const masked = (buf[1] & 0x80) !== 0;
|
||||
let pl = buf[1] & 0x7F, hl = 2;
|
||||
if (pl === 126) { if (buf.length < 4) return msgs; pl = buf.readUInt16BE(2); hl = 4; }
|
||||
else if (pl === 127) { if (buf.length < 10) return msgs; pl = Number(buf.readBigUInt64BE(2)); hl = 10; }
|
||||
if (masked) hl += 4;
|
||||
const tl = hl + pl;
|
||||
if (buf.length < tl) return msgs;
|
||||
let payload = buf.slice(hl, hl + pl);
|
||||
if (masked) {
|
||||
const mk = buf.slice(hl - 4, hl);
|
||||
const u = Buffer.alloc(pl);
|
||||
for (let i = 0; i < pl; i++) u[i] = payload[i] ^ mk[i % 4];
|
||||
payload = u;
|
||||
}
|
||||
msgs.push({ op, payload: payload.toString('utf8') });
|
||||
buf = buf.slice(tl);
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
|
||||
function connect(station) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sock = net.createConnection(WS_PORT, WS_HOST, () => {
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
const enc = Buffer.from(station.id + ':' + PASSWORD).toString('base64');
|
||||
sock.write('GET ' + station.path + ' HTTP/1.1\r\nHost: ' + WS_HOST + ':' + WS_PORT + '\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ' + key + '\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: ocpp2.0.1\r\nAuthorization: Basic ' + enc + '\r\n\r\n');
|
||||
});
|
||||
let buf = Buffer.alloc(0), ready = false;
|
||||
const sp = new Map();
|
||||
sock.on('data', (data) => {
|
||||
buf = Buffer.concat([buf, data]);
|
||||
if (!ready) {
|
||||
const s = buf.toString('utf8');
|
||||
const he = s.indexOf('\r\n\r\n');
|
||||
if (he === -1) return;
|
||||
const sl = s.split('\r\n')[0];
|
||||
if (sl.includes('101')) { ready = true; buf = buf.slice(he + 4); resolve({ sock, station, sendCall: mkSend(sp, sock), sp }); }
|
||||
else { console.log('[' + station.id + '] Handshake: ' + sl); sock.end(); reject(new Error('fail')); }
|
||||
return;
|
||||
}
|
||||
const msgs = parseFrames(buf);
|
||||
for (const m of msgs) { if (m.op === 0x1) handleMsg(m.payload, sp, sock); else if (m.op === 0x8) sock.end(); }
|
||||
});
|
||||
sock.on('error', (e) => { console.error('[' + station.id + '] ' + e.message); reject(e); });
|
||||
sock.on('close', () => console.log('[' + station.id + '] DC'));
|
||||
setTimeout(() => reject(new Error('timeout')), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
function mkSend(sp, sock) {
|
||||
return function(action, payload) {
|
||||
return new Promise((res, rej) => {
|
||||
const id = String(++msgId);
|
||||
sp.set(id, { res, rej });
|
||||
sock.write(encodeFrame(JSON.stringify([2, id, action, payload])));
|
||||
setTimeout(() => { if (sp.has(id)) { sp.delete(id); rej(new Error('timeout:' + action)); } }, 15000);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function handleMsg(msg, sp, sock) {
|
||||
try {
|
||||
const d = JSON.parse(msg);
|
||||
if (d[0] === 3 && sp.has(d[1])) { sp.get(d[1]).res(d[2]); sp.delete(d[1]); }
|
||||
else if (d[0] === 2) { sock.write(encodeFrame(JSON.stringify([3, d[1], {}]))); }
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function bootStation(station) {
|
||||
// Retry connection up to 10 times with 3s delay
|
||||
for (let attempt = 1; attempt <= 10; attempt++) {
|
||||
try {
|
||||
const { sock, sendCall } = await connect(station);
|
||||
console.log('[' + station.id + '] Connected (attempt ' + attempt + ')');
|
||||
const boot = await sendCall('BootNotification', { chargingStation: { model: station.id, vendorName: 'Cariflex', firmwareVersion: '1.0.0', serialNumber: station.id }, reason: 'PowerUp' });
|
||||
console.log('[' + station.id + '] Boot: ' + boot.status);
|
||||
await sendCall('StatusNotification', { evseId: 1, connectorId: 1, connectorStatus: 'Available', timestamp: new Date().toISOString() });
|
||||
console.log('[' + station.id + '] Available');
|
||||
return { sock, sendCall };
|
||||
} catch(e) {
|
||||
console.log('[' + station.id + '] Attempt ' + attempt + ' failed: ' + e.message);
|
||||
if (attempt < 10) await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
throw new Error('Failed after 10 attempts');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tenantId = '1';
|
||||
console.log('Simulating ' + STATIONS.length + ' stations...');
|
||||
const online = [];
|
||||
for (const st of STATIONS) {
|
||||
try { const s = await bootStation(st); online.push(Object.assign({}, st, s)); await new Promise(r => setTimeout(r, 300)); }
|
||||
catch(e) { console.error('[' + st.id + '] ' + e.message); }
|
||||
}
|
||||
console.log('\n=== ' + online.length + '/' + STATIONS.length + ' ONLINE ===');
|
||||
setInterval(async () => { for (const s of online) { try { await s.sendCall('Heartbeat', {}); } catch(e) {} } console.log('[HB] ' + online.length); }, 60000);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => { console.log('exit'); process.exit(0); });
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
281
scripts/ocpp-simulator.js
Normal file
281
scripts/ocpp-simulator.js
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OCPP 2.0.1 Charging Station Simulator for Cariflex/CitrineOS
|
||||
* Uses raw TCP WebSocket (bypassing ws module subprotocol issues)
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const CONFIG = {
|
||||
host: process.env.OCPP_HOST || 'localhost',
|
||||
port: parseInt(process.env.OCPP_PORT || '8082'),
|
||||
path: '/1/CP001',
|
||||
stationId: 'CP001',
|
||||
password: 'DEADBEEFDEADBEEF',
|
||||
vendorName: 'Cariflex',
|
||||
firmwareVersion: '1.0.0',
|
||||
};
|
||||
|
||||
let msgId = 0;
|
||||
const pending = new Map();
|
||||
let socket = null;
|
||||
let buffer = Buffer.alloc(0);
|
||||
let wsReady = false;
|
||||
|
||||
function encodeFrame(payload) {
|
||||
const maskKey = crypto.randomBytes(4);
|
||||
const payloadBuf = Buffer.from(payload, 'utf8');
|
||||
const len = payloadBuf.length;
|
||||
|
||||
let frameLen;
|
||||
if (len < 126) {
|
||||
frameLen = 6 + len; // 2 header + 4 mask + payload
|
||||
} else if (len < 65536) {
|
||||
frameLen = 8 + len; // 2 header + 2 ext len + 4 mask + payload
|
||||
} else {
|
||||
frameLen = 16 + len; // 2 header + 8 ext len + 4 mask + payload
|
||||
}
|
||||
|
||||
const frame = Buffer.alloc(frameLen);
|
||||
frame[0] = 0x81; // FIN + text
|
||||
|
||||
if (len < 126) {
|
||||
frame[1] = 0x80 | len;
|
||||
} else if (len < 65536) {
|
||||
frame[1] = 0x80 | 126;
|
||||
frame.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
frame[1] = 0x80 | 127;
|
||||
frame.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
|
||||
maskKey.copy(frame, frameLen - len - 4);
|
||||
for (let i = 0; i < len; i++) {
|
||||
frame[frameLen - len + i] = payloadBuf[i] ^ maskKey[i % 4];
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
function parseFrames() {
|
||||
const messages = [];
|
||||
|
||||
while (buffer.length >= 2) {
|
||||
const firstByte = buffer[0];
|
||||
const secondByte = buffer[1];
|
||||
const opcode = firstByte & 0x0F;
|
||||
const masked = (secondByte & 0x80) !== 0;
|
||||
let payloadLen = secondByte & 0x7F;
|
||||
let headerLen = 2;
|
||||
|
||||
if (payloadLen === 126) {
|
||||
if (buffer.length < 4) return messages;
|
||||
payloadLen = buffer.readUInt16BE(2);
|
||||
headerLen = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buffer.length < 10) return messages;
|
||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
||||
headerLen = 10;
|
||||
}
|
||||
|
||||
if (masked) headerLen += 4;
|
||||
|
||||
const totalLen = headerLen + payloadLen;
|
||||
if (buffer.length < totalLen) return messages;
|
||||
|
||||
let payload = buffer.slice(headerLen, headerLen + payloadLen);
|
||||
|
||||
if (masked) {
|
||||
const maskKey = buffer.slice(headerLen - 4, headerLen);
|
||||
const unmasked = Buffer.alloc(payloadLen);
|
||||
for (let i = 0; i < payloadLen; i++) {
|
||||
unmasked[i] = payload[i] ^ maskKey[i % 4];
|
||||
}
|
||||
payload = unmasked;
|
||||
}
|
||||
|
||||
messages.push({ opcode, payload: payload.toString('utf8') });
|
||||
buffer = buffer.slice(totalLen);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
function sendCall(action, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = String(++msgId);
|
||||
const msg = JSON.stringify([2, id, action, payload]);
|
||||
pending.set(id, { resolve, reject });
|
||||
console.log(`[SEND] ${action} (${id})`);
|
||||
socket.write(encodeFrame(msg));
|
||||
setTimeout(() => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id);
|
||||
reject(new Error(`Timeout: ${action}`));
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
try {
|
||||
const data = JSON.parse(msg);
|
||||
if (data[0] === 3) { // CallResult
|
||||
const id = data[1];
|
||||
if (pending.has(id)) {
|
||||
pending.get(id).resolve(data[2]);
|
||||
pending.delete(id);
|
||||
}
|
||||
} else if (data[0] === 2) { // Call from server
|
||||
const id = data[1];
|
||||
const action = data[2];
|
||||
console.log(`[CALL] ${action} from server`);
|
||||
const response = JSON.stringify([3, id, {}]);
|
||||
socket.write(encodeFrame(response));
|
||||
} else if (data[0] === 4) { // Error
|
||||
const id = data[1];
|
||||
console.log(`[ERROR] ${data[2]}: ${data[3]}`);
|
||||
if (pending.has(id)) {
|
||||
pending.get(id).reject(new Error(`${data[2]}: ${data[3]}`));
|
||||
pending.delete(id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[PARSE ERROR]', e.message, '- raw:', JSON.stringify(msg.substring(0, 100)));
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Connecting to ${CONFIG.host}:${CONFIG.port}${CONFIG.path}...`);
|
||||
|
||||
socket = net.createConnection(CONFIG.port, CONFIG.host, () => {
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
const encoded = Buffer.from(CONFIG.stationId + ':' + CONFIG.password).toString('base64');
|
||||
|
||||
const handshake = `GET ${CONFIG.path} HTTP/1.1\r\n` +
|
||||
`Host: ${CONFIG.host}:${CONFIG.port}\r\n` +
|
||||
`Upgrade: websocket\r\n` +
|
||||
`Connection: Upgrade\r\n` +
|
||||
`Sec-WebSocket-Key: ${key}\r\n` +
|
||||
`Sec-WebSocket-Version: 13\r\n` +
|
||||
`Sec-WebSocket-Protocol: ocpp2.0.1\r\n` +
|
||||
`Authorization: Basic ${encoded}\r\n` +
|
||||
`\r\n`;
|
||||
|
||||
socket.write(handshake);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buffer = Buffer.concat([buffer, data]);
|
||||
|
||||
if (!wsReady) {
|
||||
const str = buffer.toString('utf8');
|
||||
const headerEnd = str.indexOf('\r\n\r\n');
|
||||
if (headerEnd === -1) return; // Wait for full HTTP response
|
||||
|
||||
const statusLine = str.split('\r\n')[0];
|
||||
console.log('Handshake:', statusLine);
|
||||
|
||||
if (statusLine.includes('101')) {
|
||||
wsReady = true;
|
||||
buffer = buffer.slice(headerEnd + 4); // Skip past HTTP headers
|
||||
console.log('✅ WebSocket connected!');
|
||||
resolve();
|
||||
} else {
|
||||
console.log('❌ Handshake failed');
|
||||
console.log(str.substring(0, 500));
|
||||
socket.end();
|
||||
reject(new Error('Handshake failed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse WebSocket frames
|
||||
const messages = parseFrames();
|
||||
for (const msg of messages) {
|
||||
if (msg.opcode === 0x1) { // Text frame
|
||||
handleMessage(msg.payload);
|
||||
} else if (msg.opcode === 0x8) { // Close frame
|
||||
console.log('[CLOSE] Server sent close frame');
|
||||
socket.end();
|
||||
} else if (msg.opcode === 0x9) { // Ping
|
||||
// Send pong
|
||||
socket.write(encodeFrame(''));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (e) => {
|
||||
console.error('[SOCKET ERROR]', e.message);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('[DISCONNECTED]');
|
||||
});
|
||||
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await connect();
|
||||
|
||||
// BootNotification
|
||||
console.log('Sending BootNotification...');
|
||||
const bootResult = await sendCall('BootNotification', {
|
||||
chargingStation: {
|
||||
model: CONFIG.stationId,
|
||||
vendorName: CONFIG.vendorName,
|
||||
firmwareVersion: CONFIG.firmwareVersion,
|
||||
serialNumber: CONFIG.stationId,
|
||||
},
|
||||
reason: 'PowerUp',
|
||||
});
|
||||
console.log('✅ BootNotification result:', bootResult);
|
||||
|
||||
// StatusNotification - Available
|
||||
console.log('Sending StatusNotification (Available)...');
|
||||
await sendCall('StatusNotification', {
|
||||
evseId: 1,
|
||||
connectorId: 1,
|
||||
connectorStatus: 'Available',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
console.log('✅ Connector 1 is Available');
|
||||
|
||||
// Heartbeat
|
||||
console.log('Sending Heartbeat...');
|
||||
const hbResult = await sendCall('Heartbeat', {});
|
||||
console.log('✅ Heartbeat result:', hbResult);
|
||||
|
||||
console.log('');
|
||||
console.log('=== ✅ Station CP001 is ONLINE and READY ===');
|
||||
console.log('=== Press Ctrl+C to stop ===');
|
||||
|
||||
// Keep alive with heartbeats every 60s
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await sendCall('Heartbeat', {});
|
||||
console.log('[HEARTBEAT] ✅');
|
||||
} catch (e) {
|
||||
console.error('[HEARTBEAT] ❌', e.message);
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ Failed:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
if (socket) socket.end();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
run();
|
||||
133
scripts/ocpp-sp0-connector.js
Normal file
133
scripts/ocpp-sp0-connector.js
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OCPP 2.0.1 Security Profile 0 connector for Cariflex/CitrineOS
|
||||
* Connects via WebSocket to CSMS on port 8081 (no auth) to update ocppConnectionName
|
||||
* The main simulator handles port 8082 (Security Profile 1, Basic Auth) for actual OCPP messages
|
||||
*/
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const WS_HOST = process.env.OCPP_HOST || 'cariflex-citrineos-server';
|
||||
const WS_PORT = parseInt(process.env.OCPP_PORT || '8081');
|
||||
|
||||
function encodeFrame(payload) {
|
||||
const maskKey = crypto.randomBytes(4);
|
||||
const pb = Buffer.from(payload, 'utf8');
|
||||
const len = pb.length;
|
||||
const fl = len < 126 ? 6 + len : 8 + len;
|
||||
const f = Buffer.alloc(fl);
|
||||
f[0] = 0x81;
|
||||
if (len < 126) { f[1] = 0x80 | len; }
|
||||
else { f[1] = 0x80 | 126; f.writeUInt16BE(len, 2); }
|
||||
maskKey.copy(f, fl - len - 4);
|
||||
for (let i = 0; i < len; i++) f[fl - len + i] = pb[i] ^ maskKey[i % 4];
|
||||
return f;
|
||||
}
|
||||
|
||||
function parseFrames(buf) {
|
||||
const msgs = [];
|
||||
while (buf.length >= 2) {
|
||||
const op = buf[0] & 0x0F;
|
||||
const masked = (buf[1] & 0x80) !== 0;
|
||||
let pl = buf[1] & 0x7F, hl = 2;
|
||||
if (pl === 126) { if (buf.length < 4) return msgs; pl = buf.readUInt16BE(2); hl = 4; }
|
||||
else if (pl === 127) { if (buf.length < 10) return msgs; pl = Number(buf.readBigUInt64BE(2)); hl = 10; }
|
||||
if (masked) hl += 4;
|
||||
const tl = hl + pl;
|
||||
if (buf.length < tl) return msgs;
|
||||
let payload = buf.slice(hl, hl + pl);
|
||||
if (masked) {
|
||||
const mk = buf.slice(hl - 4, hl);
|
||||
const u = Buffer.alloc(pl);
|
||||
for (let i = 0; i < pl; i++) u[i] = payload[i] ^ mk[i % 4];
|
||||
payload = u;
|
||||
}
|
||||
msgs.push({ op, payload: payload.toString('utf8') });
|
||||
buf = buf.slice(tl);
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
|
||||
async function connectStation(stationId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sock = net.createConnection(WS_PORT, WS_HOST, () => {
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
const handshake = `GET /${stationId} HTTP/1.1\r\nHost: ${WS_HOST}:${WS_PORT}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: ocpp2.0.1\r\n\r\n`;
|
||||
sock.write(handshake);
|
||||
});
|
||||
let buf = Buffer.alloc(0), ready = false, msgId = 0;
|
||||
const pending = new Map();
|
||||
sock.on('data', (data) => {
|
||||
buf = Buffer.concat([buf, data]);
|
||||
if (!ready) {
|
||||
const s = buf.toString('utf8');
|
||||
const he = s.indexOf('\r\n\r\n');
|
||||
if (he === -1) return;
|
||||
const sl = s.split('\r\n')[0];
|
||||
if (sl.includes('101')) {
|
||||
ready = true;
|
||||
buf = buf.slice(he + 4);
|
||||
// Send BootNotification
|
||||
const id = String(++msgId);
|
||||
const payload = JSON.stringify([2, id, 'BootNotification', JSON.stringify({ chargingStation: { model: stationId, vendorName: 'Cariflex', firmwareVersion: '1.0.0', serialNumber: stationId }, reason: 'PowerUp' })]);
|
||||
sock.write(encodeFrame(payload));
|
||||
pending.set(id, { resolve: () => {}, reject: () => {} });
|
||||
} else {
|
||||
sock.end();
|
||||
reject(new Error('Handshake failed: ' + sl));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const msgs = parseFrames(buf);
|
||||
for (const m of msgs) {
|
||||
if (m.op === 0x1) {
|
||||
try {
|
||||
const d = JSON.parse(m.payload);
|
||||
if (d[0] === 3) {
|
||||
console.log('[' + stationId + '] Boot: ' + d[2].status);
|
||||
// Now send StatusNotification
|
||||
const id2 = String(++msgId);
|
||||
const sn = JSON.stringify([2, id2, 'StatusNotification', JSON.stringify({ evseId: 1, connectorId: 1, connectorStatus: 'Available', timestamp: new Date().toISOString() })]);
|
||||
sock.write(encodeFrame(sn));
|
||||
console.log('[' + stationId + '] StatusNotification sent');
|
||||
}
|
||||
// Respond to server calls
|
||||
if (d[0] === 2) {
|
||||
sock.write(encodeFrame(JSON.stringify([3, d[1], {}])));
|
||||
}
|
||||
} catch(e) {}
|
||||
if (d[0] === 3 && d[2] && d[2].status === 'Accepted') {
|
||||
resolve({ sock, pending });
|
||||
}
|
||||
} else if (m.op === 0x8) {
|
||||
sock.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
sock.on('error', (e) => { console.error('[' + stationId + '] ' + e.message); reject(e); });
|
||||
sock.on('close', () => console.log('[' + stationId + '] DC'));
|
||||
setTimeout(() => reject(new Error('timeout')), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const stations = Array.from({length: 15}, (_, i) => 'CP' + String(i+1).padStart(3, '0'));
|
||||
console.log('Connecting ' + stations.length + ' stations to port ' + WS_PORT + ' (Security Profile 0)...');
|
||||
const connections = [];
|
||||
for (const st of stations) {
|
||||
try {
|
||||
const conn = await connectStation(st);
|
||||
connections.push({ id: st, ...conn });
|
||||
console.log('[' + st + '] Connected!');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
} catch(e) {
|
||||
console.error('[' + st + '] Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
console.log('\n=== ' + connections.length + '/' + stations.length + ' connected on SP0 ===');
|
||||
// Keep alive
|
||||
setInterval(() => {}, 60000);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => { console.log('exit'); process.exit(0); });
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
1
snapshots/20260615_163350/citrineos-config.json
Normal file
1
snapshots/20260615_163350/citrineos-config.json
Normal file
@@ -0,0 +1 @@
|
||||
{"server":{"host":"0.0.0.0","port":8080},"database":{"host":"cariflex-citrineos-db","port":5432,"database":"citrine","username":"citrine","password":"citrine"},"amqp":{"host":"amqp-broker","port":5672,"username":"guest","password":"guest"},"logging":{"level":"info"}}
|
||||
10
snapshots/20260615_163350/citrineos-core.env
Normal file
10
snapshots/20260615_163350/citrineos-core.env
Normal file
@@ -0,0 +1,10 @@
|
||||
NEXT_PUBLIC_AUTH_PROVIDER=generic
|
||||
NEXT_PUBLIC_API_URL=https://hasura.digitribe.fr/v1/graphql
|
||||
NEXT_PUBLIC_CITRINE_CORE_URL=https://citrineos-core.digitribe.fr
|
||||
HASURA_ADMIN_SECRET=Digitribe972
|
||||
NEXT_PUBLIC_TENANT_ID=1
|
||||
NEXT_PUBLIC_ADMIN_EMAIL=admin@digitribe.fr
|
||||
ADMIN_PASSWORD=Digitribe972
|
||||
NEXT_PUBLIC_WS_URL=wss://hasura.digitribe.fr/v1/graphql
|
||||
NEXTAUTH_SECRET=C1tR1n30S2026S3cr3t
|
||||
NEXTAUTH_URL=https://citrineos.digitribe.fr
|
||||
6381
snapshots/20260615_163350/citrineos-db.sql
Normal file
6381
snapshots/20260615_163350/citrineos-db.sql
Normal file
File diff suppressed because one or more lines are too long
9
snapshots/20260615_163350/containers.txt
Normal file
9
snapshots/20260615_163350/containers.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
cariflex-citrineos-operator-ui citrineos-core-main-citrine-ui:latest Up 3 hours
|
||||
cariflex-hasura hasura/graphql-engine:v2.40.0 Up 4 hours (healthy)
|
||||
cariflex-ocpp-simulator config-ocpp-simulator Up 5 hours
|
||||
f33f58046fca_cariflex-amqp rabbitmq:3-management Up 5 hours (healthy)
|
||||
cariflex-everest-manager everest-manager:latest Restarting (1) 11 seconds ago
|
||||
cariflex-everest-nodered ghcr.io/everest/everest-demo/nodered:0.0.16 Up 17 hours (healthy)
|
||||
cariflex-everest-mqtt ghcr.io/everest/everest-demo/mqtt-server:0.0.16 Up 17 hours
|
||||
cariflex-citrineos-server ghcr.io/citrineos/citrineos-server:latest Up 4 days
|
||||
cariflex-citrineos-db postgis/postgis:16-3.5 Up 4 days (healthy)
|
||||
158
snapshots/20260615_163350/docker-compose-citrineos-everest.yml
Normal file
158
snapshots/20260615_163350/docker-compose-citrineos-everest.yml
Normal file
@@ -0,0 +1,158 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
citrineos-server:
|
||||
image: ghcr.io/citrineos/citrineos-server:latest
|
||||
container_name: cariflex-citrineos-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_NAME: "all"
|
||||
APP_ENV: "docker"
|
||||
AWS_REGION: us-east-1
|
||||
AWS_ACCESS_KEY_ID: minioadmin
|
||||
AWS_SECRET_ACCESS_KEY: minioadmin
|
||||
DB_STRATEGY: "migrate"
|
||||
BOOTSTRAP_CITRINEOS_DATABASE_HOST: "cariflex-citrineos-db"
|
||||
BOOTSTRAP_CITRINEOS_CONFIG_FILENAME: "config.json"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE: "local"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_FILE_PATH: "/data"
|
||||
CONFIG_CITRINEOS_WIPE_FILE_ON_START: "true"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
cariflex-amqp:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- citrineos-data:/data
|
||||
ports:
|
||||
- "8081:8080"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-citrineos-db:
|
||||
image: postgis/postgis:16-3.5
|
||||
container_name: cariflex-citrineos-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: citrine
|
||||
POSTGRES_USER: citrine
|
||||
POSTGRES_PASSWORD: citrine
|
||||
volumes:
|
||||
- citrineos-db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready --username=citrine
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-amqp:
|
||||
image: rabbitmq:3-management
|
||||
container_name: cariflex-amqp
|
||||
networks:
|
||||
cariflex-internal:
|
||||
aliases:
|
||||
- amqp-broker
|
||||
traefik-public:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.rabbitmq.rule=Host(`amqp.digitribe.fr`)"
|
||||
- "traefik.http.routers.rabbitmq.entrypoints=websecure"
|
||||
- "traefik.http.routers.rabbitmq.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.rabbitmq.loadbalancer.server.port=15672"
|
||||
volumes:
|
||||
- citrineos-amqp-data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: rabbitmq-diagnostics -q ping
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
hasura:
|
||||
image: hasura/graphql-engine:v2.40.0
|
||||
container_name: cariflex-hasura
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:8080"
|
||||
environment:
|
||||
HASURA_GRAPHQL_DATABASE_URL: "postgresql://citrine:***@cariflex-citrineos-db:5432/citrine"
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: "Digitribe972"
|
||||
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "anonymous"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.hasura.rule=Host(`hasura.digitribe.fr`)"
|
||||
- "traefik.http.routers.hasura.entrypoints=websecure"
|
||||
- "traefik.http.routers.hasura.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.hasura.loadbalancer.server.port=8080"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
citrineos-operator-ui:
|
||||
image: citrineos-core-main-citrine-ui:latest
|
||||
container_name: cariflex-citrineos-operator-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3002:3000"
|
||||
environment:
|
||||
NEXTAUTH_SECRET: Digitribe972
|
||||
ADMIN_PASSWORD: Digitribe972
|
||||
depends_on:
|
||||
- hasura
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.citrineos-ui.rule=Host(`citrineos.digitribe.fr`)"
|
||||
- "traefik.http.routers.citrineos-ui.entrypoints=websecure"
|
||||
- "traefik.http.routers.citrineos-ui.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.citrineos-ui.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
# === EVerest (simulateur de charge OCPP 2.0.1) ===
|
||||
everest-mqtt:
|
||||
image: ghcr.io/everest/everest-demo/mqtt-server:0.0.16
|
||||
container_name: cariflex-everest-mqtt
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
everest-nodered:
|
||||
image: ghcr.io/everest/everest-demo/nodered:0.0.16
|
||||
container_name: cariflex-everest-nodered
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- everest-mqtt
|
||||
environment:
|
||||
- MQTT_SERVER_ADDRESS=everest-mqtt
|
||||
- FLOWS=/config/config-sil-two-evse-flow.json
|
||||
networks:
|
||||
- cariflex-internal
|
||||
ports:
|
||||
- "1880:1880"
|
||||
|
||||
volumes:
|
||||
citrineos-data:
|
||||
driver: local
|
||||
citrineos-db-data:
|
||||
driver: local
|
||||
citrineos-amqp-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
cariflex-internal:
|
||||
name: config_cariflex-internal
|
||||
external: true
|
||||
171
snapshots/20260615_163350/docker-compose-citrineos.yml
Normal file
171
snapshots/20260615_163350/docker-compose-citrineos.yml
Normal file
@@ -0,0 +1,171 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
citrineos-server:
|
||||
image: ghcr.io/citrineos/citrineos-server:latest
|
||||
container_name: cariflex-citrineos-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_NAME: "all"
|
||||
APP_ENV: "docker"
|
||||
AWS_REGION: us-east-1
|
||||
AWS_ACCESS_KEY_ID: minioadmin
|
||||
AWS_SECRET_ACCESS_KEY: minioadmin
|
||||
DB_STRATEGY: "migrate"
|
||||
BOOTSTRAP_CITRINEOS_DATABASE_HOST: "cariflex-citrineos-db"
|
||||
BOOTSTRAP_CITRINEOS_CONFIG_FILENAME: "config.json"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE: "local"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_FILE_PATH: "/data"
|
||||
CONFIG_CITRINEOS_WIPE_FILE_ON_START: "true"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
cariflex-amqp:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- citrineos-data:/data
|
||||
ports:
|
||||
- "8081:8080"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-citrineos-db:
|
||||
image: postgis/postgis:16-3.5
|
||||
container_name: cariflex-citrineos-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: citrine
|
||||
POSTGRES_USER: citrine
|
||||
POSTGRES_PASSWORD: citrine
|
||||
volumes:
|
||||
- citrineos-db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready --username=citrine
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-amqp:
|
||||
image: rabbitmq:3-management
|
||||
container_name: cariflex-amqp
|
||||
networks:
|
||||
cariflex-internal:
|
||||
aliases:
|
||||
- amqp-broker
|
||||
traefik-public:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.rabbitmq.rule=Host(`amqp.digitribe.fr`)"
|
||||
- "traefik.http.routers.rabbitmq.entrypoints=websecure"
|
||||
- "traefik.http.routers.rabbitmq.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.rabbitmq.loadbalancer.server.port=15672"
|
||||
volumes:
|
||||
- citrineos-amqp-data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: rabbitmq-diagnostics -q ping
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
hasura:
|
||||
image: hasura/graphql-engine:v2.40.0
|
||||
container_name: cariflex-hasura
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:8080"
|
||||
environment:
|
||||
HASURA_GRAPHQL_DATABASE_URL: "postgresql://citrine:citrine@cariflex-citrineos-db:5432/citrine"
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: "Digitribe972"
|
||||
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "anonymous"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.hasura.rule=Host(`hasura.digitribe.fr`)"
|
||||
- "traefik.http.routers.hasura.entrypoints=websecure"
|
||||
- "traefik.http.routers.hasura.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.hasura.loadbalancer.server.port=8080"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
citrineos-operator-ui:
|
||||
image: citrineos-core-main-citrine-ui:latest
|
||||
container_name: cariflex-citrineos-operator-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3002:3000"
|
||||
environment:
|
||||
NEXTAUTH_SECRET: Digitribe972
|
||||
ADMIN_PASSWORD: Digitribe972
|
||||
depends_on:
|
||||
- hasura
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.citrineos-ui.rule=Host(`citrineos.digitribe.fr`)"
|
||||
- "traefik.http.routers.citrineos-ui.entrypoints=websecure"
|
||||
- "traefik.http.routers.citrineos-ui.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.citrineos-ui.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
# === EVerest MQTT + NodeRED (UI de contrôle) ===
|
||||
everest-mqtt:
|
||||
image: ghcr.io/everest/everest-demo/mqtt-server:0.0.16
|
||||
container_name: cariflex-everest-mqtt
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
everest-nodered:
|
||||
image: ghcr.io/everest/everest-demo/nodered:0.0.16
|
||||
container_name: cariflex-everest-nodered
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- everest-mqtt
|
||||
environment:
|
||||
- MQTT_SERVER_ADDRESS=everest-mqtt
|
||||
- FLOWS=/config/config-sil-two-evse-flow.json
|
||||
networks:
|
||||
- cariflex-internal
|
||||
ports:
|
||||
- "1880:1880"
|
||||
|
||||
# === OCPP 2.0.1 Simulators ===
|
||||
ocpp-simulator:
|
||||
build:
|
||||
context: /home/eric/cariflex/scripts
|
||||
dockerfile: Dockerfile.simulator
|
||||
container_name: cariflex-ocpp-simulator
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
OCPP_HOST: "cariflex-citrineos-server"
|
||||
OCPP_PORT: "8082"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
volumes:
|
||||
citrineos-data:
|
||||
driver: local
|
||||
citrineos-db-data:
|
||||
driver: local
|
||||
citrineos-amqp-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
cariflex-internal:
|
||||
name: config_cariflex-internal
|
||||
external: true
|
||||
1
snapshots/20260615_163350/hasura-metadata.json
Normal file
1
snapshots/20260615_163350/hasura-metadata.json
Normal file
File diff suppressed because one or more lines are too long
5
snapshots/20260615_163350/scripts/Dockerfile.simulator
Normal file
5
snapshots/20260615_163350/scripts/Dockerfile.simulator
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY ocpp-simulator.js .
|
||||
COPY ocpp-simulator-multi.js .
|
||||
CMD ["node", "ocpp-simulator-multi.js"]
|
||||
5
snapshots/20260615_163350/scripts/Dockerfile.sp0
Normal file
5
snapshots/20260615_163350/scripts/Dockerfile.sp0
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY ocpp-simulator-multi.js .
|
||||
COPY ocpp-sp0-connector.js .
|
||||
CMD ["node", "ocpp-sp0-connector.js"]
|
||||
29
snapshots/20260615_163350/scripts/configure-auth.py
Normal file
29
snapshots/20260615_163350/scripts/configure-auth.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Configure BasicAuthPassword for all 15 Cariflex charging stations"""
|
||||
import json, time, urllib.request, urllib.error
|
||||
|
||||
CITRINEOS_URL = "http://localhost:8081"
|
||||
PASSWORD = "DEADBEEFDEADBEEF"
|
||||
|
||||
for i in range(1, 16):
|
||||
cp_id = f"CP{i:03d}"
|
||||
url = f"{CITRINEOS_URL}/data/monitoring/variableAttribute?stationId={cp_id}&setOnCharger=true"
|
||||
payload = json.dumps({
|
||||
"component": {"name": "SecurityCtrlr"},
|
||||
"variable": {"name": "BasicAuthPassword"},
|
||||
"variableAttribute": [{"value": PASSWORD}],
|
||||
"variableCharacteristics": {"dataType": "passwordString", "supportsMonitoring": False}
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(url, data=payload, method='PUT',
|
||||
headers={'Content-Type': 'application/json'})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
print(f"OK {cp_id}: HTTP {resp.status}")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"FAIL {cp_id}: HTTP {e.code}")
|
||||
except Exception as e:
|
||||
print(f"FAIL {cp_id}: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
print("Done")
|
||||
30
snapshots/20260615_163350/scripts/configure-ocpi-everest.sh
Normal file
30
snapshots/20260615_163350/scripts/configure-ocpi-everest.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# === Post-deploy OCPI + Everest configuration for Cariflex ===
|
||||
# Run this AFTER docker compose up succeeds
|
||||
|
||||
set -e
|
||||
|
||||
CSMS_URL="http://localhost:8081"
|
||||
GRAPHQL_URL="http://localhost:8090/v1/graphql"
|
||||
DB_CONTAINER="cariflex-citrineos-db"
|
||||
|
||||
echo "=== Step 1: Seed OCPI database ==="
|
||||
# Run inside the OCPI container to seed default tenant + emsp
|
||||
docker exec cariflex-ocpi npm run seed-db 2>&1 || echo "Seed may have run before, continuing..."
|
||||
|
||||
echo "=== Step 2: Verify Citiveness ==="
|
||||
# Check CitrineOS Core is healthy
|
||||
curl -sf "${CSMS_URL}/ocpp/health" && echo "CitrineOS Core: OK" || echo "CitrineOS Core: UNHEALTHY"
|
||||
|
||||
echo "=== Step 3: Configure EVerest Manager OCPP target ==="
|
||||
# The manager already points to ws://cariflex-citrineos-server:8080/cp001 via Dockerfile
|
||||
# Check manager is running
|
||||
docker ps --filter "name=cariflex-everest-manager" --format "{{.Status}}" && echo "Everest Manager: running" || echo "Everest Manager: NOT RUNNING"
|
||||
|
||||
echo "=== Step 4: Verify UI connectivity ==="
|
||||
echo " Everest NodeRED UI: http://localhost:1880/ui/"
|
||||
echo " Everest OCPP Logs: http://localhost:8888"
|
||||
echo " CitrineOS UI: https://citrineos.digitribe.fr"
|
||||
echo " Hasura Console: https://hasura.digitribe.fr"
|
||||
echo ""
|
||||
echo "=== DONE ==="
|
||||
133
snapshots/20260615_163350/scripts/ocpp-simulator-multi.js
Normal file
133
snapshots/20260615_163350/scripts/ocpp-simulator-multi.js
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PASSWORD='DEADBEEFDEADBEEF';
|
||||
const STATIONS = Array.from({length: 15}, (_, i) => ({
|
||||
id: `CP${String(i+1).padStart(3, '0')}`,
|
||||
path: `/1/CP${String(i+1).padStart(3, '0')}`,
|
||||
}));
|
||||
|
||||
const WS_HOST = process.env.OCPP_HOST || 'cariflex-citrineos-server';
|
||||
const WS_PORT = parseInt(process.env.OCPP_PORT || '8082');
|
||||
let msgId = 0;
|
||||
|
||||
function encodeFrame(payload) {
|
||||
const maskKey = crypto.randomBytes(4);
|
||||
const pb = Buffer.from(payload, 'utf8');
|
||||
const len = pb.length;
|
||||
const fl = len < 126 ? 6 + len : 8 + len;
|
||||
const f = Buffer.alloc(fl);
|
||||
f[0] = 0x81;
|
||||
if (len < 126) { f[1] = 0x80 | len; }
|
||||
else { f[1] = 0x80 | 126; f.writeUInt16BE(len, 2); }
|
||||
maskKey.copy(f, fl - len - 4);
|
||||
for (let i = 0; i < len; i++) f[fl - len + i] = pb[i] ^ maskKey[i % 4];
|
||||
return f;
|
||||
}
|
||||
|
||||
function parseFrames(buf) {
|
||||
const msgs = [];
|
||||
while (buf.length >= 2) {
|
||||
const op = buf[0] & 0x0F;
|
||||
const masked = (buf[1] & 0x80) !== 0;
|
||||
let pl = buf[1] & 0x7F, hl = 2;
|
||||
if (pl === 126) { if (buf.length < 4) return msgs; pl = buf.readUInt16BE(2); hl = 4; }
|
||||
else if (pl === 127) { if (buf.length < 10) return msgs; pl = Number(buf.readBigUInt64BE(2)); hl = 10; }
|
||||
if (masked) hl += 4;
|
||||
const tl = hl + pl;
|
||||
if (buf.length < tl) return msgs;
|
||||
let payload = buf.slice(hl, hl + pl);
|
||||
if (masked) {
|
||||
const mk = buf.slice(hl - 4, hl);
|
||||
const u = Buffer.alloc(pl);
|
||||
for (let i = 0; i < pl; i++) u[i] = payload[i] ^ mk[i % 4];
|
||||
payload = u;
|
||||
}
|
||||
msgs.push({ op, payload: payload.toString('utf8') });
|
||||
buf = buf.slice(tl);
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
|
||||
function connect(station) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sock = net.createConnection(WS_PORT, WS_HOST, () => {
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
const enc = Buffer.from(station.id + ':' + PASSWORD).toString('base64');
|
||||
sock.write('GET ' + station.path + ' HTTP/1.1\r\nHost: ' + WS_HOST + ':' + WS_PORT + '\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ' + key + '\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: ocpp2.0.1\r\nAuthorization: Basic ' + enc + '\r\n\r\n');
|
||||
});
|
||||
let buf = Buffer.alloc(0), ready = false;
|
||||
const sp = new Map();
|
||||
sock.on('data', (data) => {
|
||||
buf = Buffer.concat([buf, data]);
|
||||
if (!ready) {
|
||||
const s = buf.toString('utf8');
|
||||
const he = s.indexOf('\r\n\r\n');
|
||||
if (he === -1) return;
|
||||
const sl = s.split('\r\n')[0];
|
||||
if (sl.includes('101')) { ready = true; buf = buf.slice(he + 4); resolve({ sock, station, sendCall: mkSend(sp, sock), sp }); }
|
||||
else { console.log('[' + station.id + '] Handshake: ' + sl); sock.end(); reject(new Error('fail')); }
|
||||
return;
|
||||
}
|
||||
const msgs = parseFrames(buf);
|
||||
for (const m of msgs) { if (m.op === 0x1) handleMsg(m.payload, sp, sock); else if (m.op === 0x8) sock.end(); }
|
||||
});
|
||||
sock.on('error', (e) => { console.error('[' + station.id + '] ' + e.message); reject(e); });
|
||||
sock.on('close', () => console.log('[' + station.id + '] DC'));
|
||||
setTimeout(() => reject(new Error('timeout')), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
function mkSend(sp, sock) {
|
||||
return function(action, payload) {
|
||||
return new Promise((res, rej) => {
|
||||
const id = String(++msgId);
|
||||
sp.set(id, { res, rej });
|
||||
sock.write(encodeFrame(JSON.stringify([2, id, action, payload])));
|
||||
setTimeout(() => { if (sp.has(id)) { sp.delete(id); rej(new Error('timeout:' + action)); } }, 15000);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function handleMsg(msg, sp, sock) {
|
||||
try {
|
||||
const d = JSON.parse(msg);
|
||||
if (d[0] === 3 && sp.has(d[1])) { sp.get(d[1]).res(d[2]); sp.delete(d[1]); }
|
||||
else if (d[0] === 2) { sock.write(encodeFrame(JSON.stringify([3, d[1], {}]))); }
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function bootStation(station) {
|
||||
// Retry connection up to 10 times with 3s delay
|
||||
for (let attempt = 1; attempt <= 10; attempt++) {
|
||||
try {
|
||||
const { sock, sendCall } = await connect(station);
|
||||
console.log('[' + station.id + '] Connected (attempt ' + attempt + ')');
|
||||
const boot = await sendCall('BootNotification', { chargingStation: { model: station.id, vendorName: 'Cariflex', firmwareVersion: '1.0.0', serialNumber: station.id }, reason: 'PowerUp' });
|
||||
console.log('[' + station.id + '] Boot: ' + boot.status);
|
||||
await sendCall('StatusNotification', { evseId: 1, connectorId: 1, connectorStatus: 'Available', timestamp: new Date().toISOString() });
|
||||
console.log('[' + station.id + '] Available');
|
||||
return { sock, sendCall };
|
||||
} catch(e) {
|
||||
console.log('[' + station.id + '] Attempt ' + attempt + ' failed: ' + e.message);
|
||||
if (attempt < 10) await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
throw new Error('Failed after 10 attempts');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tenantId = '1';
|
||||
console.log('Simulating ' + STATIONS.length + ' stations...');
|
||||
const online = [];
|
||||
for (const st of STATIONS) {
|
||||
try { const s = await bootStation(st); online.push(Object.assign({}, st, s)); await new Promise(r => setTimeout(r, 300)); }
|
||||
catch(e) { console.error('[' + st.id + '] ' + e.message); }
|
||||
}
|
||||
console.log('\n=== ' + online.length + '/' + STATIONS.length + ' ONLINE ===');
|
||||
setInterval(async () => { for (const s of online) { try { await s.sendCall('Heartbeat', {}); } catch(e) {} } console.log('[HB] ' + online.length); }, 60000);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => { console.log('exit'); process.exit(0); });
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
281
snapshots/20260615_163350/scripts/ocpp-simulator.js
Normal file
281
snapshots/20260615_163350/scripts/ocpp-simulator.js
Normal file
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OCPP 2.0.1 Charging Station Simulator for Cariflex/CitrineOS
|
||||
* Uses raw TCP WebSocket (bypassing ws module subprotocol issues)
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const CONFIG = {
|
||||
host: process.env.OCPP_HOST || 'localhost',
|
||||
port: parseInt(process.env.OCPP_PORT || '8082'),
|
||||
path: '/1/CP001',
|
||||
stationId: 'CP001',
|
||||
password: 'DEADBEEFDEADBEEF',
|
||||
vendorName: 'Cariflex',
|
||||
firmwareVersion: '1.0.0',
|
||||
};
|
||||
|
||||
let msgId = 0;
|
||||
const pending = new Map();
|
||||
let socket = null;
|
||||
let buffer = Buffer.alloc(0);
|
||||
let wsReady = false;
|
||||
|
||||
function encodeFrame(payload) {
|
||||
const maskKey = crypto.randomBytes(4);
|
||||
const payloadBuf = Buffer.from(payload, 'utf8');
|
||||
const len = payloadBuf.length;
|
||||
|
||||
let frameLen;
|
||||
if (len < 126) {
|
||||
frameLen = 6 + len; // 2 header + 4 mask + payload
|
||||
} else if (len < 65536) {
|
||||
frameLen = 8 + len; // 2 header + 2 ext len + 4 mask + payload
|
||||
} else {
|
||||
frameLen = 16 + len; // 2 header + 8 ext len + 4 mask + payload
|
||||
}
|
||||
|
||||
const frame = Buffer.alloc(frameLen);
|
||||
frame[0] = 0x81; // FIN + text
|
||||
|
||||
if (len < 126) {
|
||||
frame[1] = 0x80 | len;
|
||||
} else if (len < 65536) {
|
||||
frame[1] = 0x80 | 126;
|
||||
frame.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
frame[1] = 0x80 | 127;
|
||||
frame.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
|
||||
maskKey.copy(frame, frameLen - len - 4);
|
||||
for (let i = 0; i < len; i++) {
|
||||
frame[frameLen - len + i] = payloadBuf[i] ^ maskKey[i % 4];
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
function parseFrames() {
|
||||
const messages = [];
|
||||
|
||||
while (buffer.length >= 2) {
|
||||
const firstByte = buffer[0];
|
||||
const secondByte = buffer[1];
|
||||
const opcode = firstByte & 0x0F;
|
||||
const masked = (secondByte & 0x80) !== 0;
|
||||
let payloadLen = secondByte & 0x7F;
|
||||
let headerLen = 2;
|
||||
|
||||
if (payloadLen === 126) {
|
||||
if (buffer.length < 4) return messages;
|
||||
payloadLen = buffer.readUInt16BE(2);
|
||||
headerLen = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buffer.length < 10) return messages;
|
||||
payloadLen = Number(buffer.readBigUInt64BE(2));
|
||||
headerLen = 10;
|
||||
}
|
||||
|
||||
if (masked) headerLen += 4;
|
||||
|
||||
const totalLen = headerLen + payloadLen;
|
||||
if (buffer.length < totalLen) return messages;
|
||||
|
||||
let payload = buffer.slice(headerLen, headerLen + payloadLen);
|
||||
|
||||
if (masked) {
|
||||
const maskKey = buffer.slice(headerLen - 4, headerLen);
|
||||
const unmasked = Buffer.alloc(payloadLen);
|
||||
for (let i = 0; i < payloadLen; i++) {
|
||||
unmasked[i] = payload[i] ^ maskKey[i % 4];
|
||||
}
|
||||
payload = unmasked;
|
||||
}
|
||||
|
||||
messages.push({ opcode, payload: payload.toString('utf8') });
|
||||
buffer = buffer.slice(totalLen);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
function sendCall(action, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = String(++msgId);
|
||||
const msg = JSON.stringify([2, id, action, payload]);
|
||||
pending.set(id, { resolve, reject });
|
||||
console.log(`[SEND] ${action} (${id})`);
|
||||
socket.write(encodeFrame(msg));
|
||||
setTimeout(() => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id);
|
||||
reject(new Error(`Timeout: ${action}`));
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
}
|
||||
|
||||
function handleMessage(msg) {
|
||||
try {
|
||||
const data = JSON.parse(msg);
|
||||
if (data[0] === 3) { // CallResult
|
||||
const id = data[1];
|
||||
if (pending.has(id)) {
|
||||
pending.get(id).resolve(data[2]);
|
||||
pending.delete(id);
|
||||
}
|
||||
} else if (data[0] === 2) { // Call from server
|
||||
const id = data[1];
|
||||
const action = data[2];
|
||||
console.log(`[CALL] ${action} from server`);
|
||||
const response = JSON.stringify([3, id, {}]);
|
||||
socket.write(encodeFrame(response));
|
||||
} else if (data[0] === 4) { // Error
|
||||
const id = data[1];
|
||||
console.log(`[ERROR] ${data[2]}: ${data[3]}`);
|
||||
if (pending.has(id)) {
|
||||
pending.get(id).reject(new Error(`${data[2]}: ${data[3]}`));
|
||||
pending.delete(id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[PARSE ERROR]', e.message, '- raw:', JSON.stringify(msg.substring(0, 100)));
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`Connecting to ${CONFIG.host}:${CONFIG.port}${CONFIG.path}...`);
|
||||
|
||||
socket = net.createConnection(CONFIG.port, CONFIG.host, () => {
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
const encoded = Buffer.from(CONFIG.stationId + ':' + CONFIG.password).toString('base64');
|
||||
|
||||
const handshake = `GET ${CONFIG.path} HTTP/1.1\r\n` +
|
||||
`Host: ${CONFIG.host}:${CONFIG.port}\r\n` +
|
||||
`Upgrade: websocket\r\n` +
|
||||
`Connection: Upgrade\r\n` +
|
||||
`Sec-WebSocket-Key: ${key}\r\n` +
|
||||
`Sec-WebSocket-Version: 13\r\n` +
|
||||
`Sec-WebSocket-Protocol: ocpp2.0.1\r\n` +
|
||||
`Authorization: Basic ${encoded}\r\n` +
|
||||
`\r\n`;
|
||||
|
||||
socket.write(handshake);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buffer = Buffer.concat([buffer, data]);
|
||||
|
||||
if (!wsReady) {
|
||||
const str = buffer.toString('utf8');
|
||||
const headerEnd = str.indexOf('\r\n\r\n');
|
||||
if (headerEnd === -1) return; // Wait for full HTTP response
|
||||
|
||||
const statusLine = str.split('\r\n')[0];
|
||||
console.log('Handshake:', statusLine);
|
||||
|
||||
if (statusLine.includes('101')) {
|
||||
wsReady = true;
|
||||
buffer = buffer.slice(headerEnd + 4); // Skip past HTTP headers
|
||||
console.log('✅ WebSocket connected!');
|
||||
resolve();
|
||||
} else {
|
||||
console.log('❌ Handshake failed');
|
||||
console.log(str.substring(0, 500));
|
||||
socket.end();
|
||||
reject(new Error('Handshake failed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse WebSocket frames
|
||||
const messages = parseFrames();
|
||||
for (const msg of messages) {
|
||||
if (msg.opcode === 0x1) { // Text frame
|
||||
handleMessage(msg.payload);
|
||||
} else if (msg.opcode === 0x8) { // Close frame
|
||||
console.log('[CLOSE] Server sent close frame');
|
||||
socket.end();
|
||||
} else if (msg.opcode === 0x9) { // Ping
|
||||
// Send pong
|
||||
socket.write(encodeFrame(''));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (e) => {
|
||||
console.error('[SOCKET ERROR]', e.message);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('[DISCONNECTED]');
|
||||
});
|
||||
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await connect();
|
||||
|
||||
// BootNotification
|
||||
console.log('Sending BootNotification...');
|
||||
const bootResult = await sendCall('BootNotification', {
|
||||
chargingStation: {
|
||||
model: CONFIG.stationId,
|
||||
vendorName: CONFIG.vendorName,
|
||||
firmwareVersion: CONFIG.firmwareVersion,
|
||||
serialNumber: CONFIG.stationId,
|
||||
},
|
||||
reason: 'PowerUp',
|
||||
});
|
||||
console.log('✅ BootNotification result:', bootResult);
|
||||
|
||||
// StatusNotification - Available
|
||||
console.log('Sending StatusNotification (Available)...');
|
||||
await sendCall('StatusNotification', {
|
||||
evseId: 1,
|
||||
connectorId: 1,
|
||||
connectorStatus: 'Available',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
console.log('✅ Connector 1 is Available');
|
||||
|
||||
// Heartbeat
|
||||
console.log('Sending Heartbeat...');
|
||||
const hbResult = await sendCall('Heartbeat', {});
|
||||
console.log('✅ Heartbeat result:', hbResult);
|
||||
|
||||
console.log('');
|
||||
console.log('=== ✅ Station CP001 is ONLINE and READY ===');
|
||||
console.log('=== Press Ctrl+C to stop ===');
|
||||
|
||||
// Keep alive with heartbeats every 60s
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await sendCall('Heartbeat', {});
|
||||
console.log('[HEARTBEAT] ✅');
|
||||
} catch (e) {
|
||||
console.error('[HEARTBEAT] ❌', e.message);
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ Failed:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
if (socket) socket.end();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
run();
|
||||
133
snapshots/20260615_163350/scripts/ocpp-sp0-connector.js
Normal file
133
snapshots/20260615_163350/scripts/ocpp-sp0-connector.js
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OCPP 2.0.1 Security Profile 0 connector for Cariflex/CitrineOS
|
||||
* Connects via WebSocket to CSMS on port 8081 (no auth) to update ocppConnectionName
|
||||
* The main simulator handles port 8082 (Security Profile 1, Basic Auth) for actual OCPP messages
|
||||
*/
|
||||
const net = require('net');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const WS_HOST = process.env.OCPP_HOST || 'cariflex-citrineos-server';
|
||||
const WS_PORT = parseInt(process.env.OCPP_PORT || '8081');
|
||||
|
||||
function encodeFrame(payload) {
|
||||
const maskKey = crypto.randomBytes(4);
|
||||
const pb = Buffer.from(payload, 'utf8');
|
||||
const len = pb.length;
|
||||
const fl = len < 126 ? 6 + len : 8 + len;
|
||||
const f = Buffer.alloc(fl);
|
||||
f[0] = 0x81;
|
||||
if (len < 126) { f[1] = 0x80 | len; }
|
||||
else { f[1] = 0x80 | 126; f.writeUInt16BE(len, 2); }
|
||||
maskKey.copy(f, fl - len - 4);
|
||||
for (let i = 0; i < len; i++) f[fl - len + i] = pb[i] ^ maskKey[i % 4];
|
||||
return f;
|
||||
}
|
||||
|
||||
function parseFrames(buf) {
|
||||
const msgs = [];
|
||||
while (buf.length >= 2) {
|
||||
const op = buf[0] & 0x0F;
|
||||
const masked = (buf[1] & 0x80) !== 0;
|
||||
let pl = buf[1] & 0x7F, hl = 2;
|
||||
if (pl === 126) { if (buf.length < 4) return msgs; pl = buf.readUInt16BE(2); hl = 4; }
|
||||
else if (pl === 127) { if (buf.length < 10) return msgs; pl = Number(buf.readBigUInt64BE(2)); hl = 10; }
|
||||
if (masked) hl += 4;
|
||||
const tl = hl + pl;
|
||||
if (buf.length < tl) return msgs;
|
||||
let payload = buf.slice(hl, hl + pl);
|
||||
if (masked) {
|
||||
const mk = buf.slice(hl - 4, hl);
|
||||
const u = Buffer.alloc(pl);
|
||||
for (let i = 0; i < pl; i++) u[i] = payload[i] ^ mk[i % 4];
|
||||
payload = u;
|
||||
}
|
||||
msgs.push({ op, payload: payload.toString('utf8') });
|
||||
buf = buf.slice(tl);
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
|
||||
async function connectStation(stationId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sock = net.createConnection(WS_PORT, WS_HOST, () => {
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
const handshake = `GET /${stationId} HTTP/1.1\r\nHost: ${WS_HOST}:${WS_PORT}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: ocpp2.0.1\r\n\r\n`;
|
||||
sock.write(handshake);
|
||||
});
|
||||
let buf = Buffer.alloc(0), ready = false, msgId = 0;
|
||||
const pending = new Map();
|
||||
sock.on('data', (data) => {
|
||||
buf = Buffer.concat([buf, data]);
|
||||
if (!ready) {
|
||||
const s = buf.toString('utf8');
|
||||
const he = s.indexOf('\r\n\r\n');
|
||||
if (he === -1) return;
|
||||
const sl = s.split('\r\n')[0];
|
||||
if (sl.includes('101')) {
|
||||
ready = true;
|
||||
buf = buf.slice(he + 4);
|
||||
// Send BootNotification
|
||||
const id = String(++msgId);
|
||||
const payload = JSON.stringify([2, id, 'BootNotification', JSON.stringify({ chargingStation: { model: stationId, vendorName: 'Cariflex', firmwareVersion: '1.0.0', serialNumber: stationId }, reason: 'PowerUp' })]);
|
||||
sock.write(encodeFrame(payload));
|
||||
pending.set(id, { resolve: () => {}, reject: () => {} });
|
||||
} else {
|
||||
sock.end();
|
||||
reject(new Error('Handshake failed: ' + sl));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const msgs = parseFrames(buf);
|
||||
for (const m of msgs) {
|
||||
if (m.op === 0x1) {
|
||||
try {
|
||||
const d = JSON.parse(m.payload);
|
||||
if (d[0] === 3) {
|
||||
console.log('[' + stationId + '] Boot: ' + d[2].status);
|
||||
// Now send StatusNotification
|
||||
const id2 = String(++msgId);
|
||||
const sn = JSON.stringify([2, id2, 'StatusNotification', JSON.stringify({ evseId: 1, connectorId: 1, connectorStatus: 'Available', timestamp: new Date().toISOString() })]);
|
||||
sock.write(encodeFrame(sn));
|
||||
console.log('[' + stationId + '] StatusNotification sent');
|
||||
}
|
||||
// Respond to server calls
|
||||
if (d[0] === 2) {
|
||||
sock.write(encodeFrame(JSON.stringify([3, d[1], {}])));
|
||||
}
|
||||
} catch(e) {}
|
||||
if (d[0] === 3 && d[2] && d[2].status === 'Accepted') {
|
||||
resolve({ sock, pending });
|
||||
}
|
||||
} else if (m.op === 0x8) {
|
||||
sock.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
sock.on('error', (e) => { console.error('[' + stationId + '] ' + e.message); reject(e); });
|
||||
sock.on('close', () => console.log('[' + stationId + '] DC'));
|
||||
setTimeout(() => reject(new Error('timeout')), 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const stations = Array.from({length: 15}, (_, i) => 'CP' + String(i+1).padStart(3, '0'));
|
||||
console.log('Connecting ' + stations.length + ' stations to port ' + WS_PORT + ' (Security Profile 0)...');
|
||||
const connections = [];
|
||||
for (const st of stations) {
|
||||
try {
|
||||
const conn = await connectStation(st);
|
||||
connections.push({ id: st, ...conn });
|
||||
console.log('[' + st + '] Connected!');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
} catch(e) {
|
||||
console.error('[' + st + '] Failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
console.log('\n=== ' + connections.length + '/' + stations.length + ' connected on SP0 ===');
|
||||
// Keep alive
|
||||
setInterval(() => {}, 60000);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => { console.log('exit'); process.exit(0); });
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
6381
snapshots/20260615_final/citrineos-db.sql
Normal file
6381
snapshots/20260615_final/citrineos-db.sql
Normal file
File diff suppressed because one or more lines are too long
10
snapshots/20260615_final/config/citrineos.env
Normal file
10
snapshots/20260615_final/config/citrineos.env
Normal file
@@ -0,0 +1,10 @@
|
||||
NEXT_PUBLIC_AUTH_PROVIDER=generic
|
||||
NEXT_PUBLIC_API_URL=https://hasura.digitribe.fr/v1/graphql
|
||||
NEXT_PUBLIC_CITRINE_CORE_URL=https://citrineos-core.digitribe.fr
|
||||
HASURA_ADMIN_SECRET=Digitribe972
|
||||
NEXT_PUBLIC_TENANT_ID=1
|
||||
NEXT_PUBLIC_ADMIN_EMAIL=admin@digitribe.fr
|
||||
ADMIN_PASSWORD=Digitribe972
|
||||
NEXT_PUBLIC_WS_URL=wss://hasura.digitribe.fr/v1/graphql
|
||||
NEXTAUTH_SECRET=C1tR1n30S2026S3cr3t
|
||||
NEXTAUTH_URL=https://citrineos.digitribe.fr
|
||||
@@ -0,0 +1,158 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
citrineos-server:
|
||||
image: ghcr.io/citrineos/citrineos-server:latest
|
||||
container_name: cariflex-citrineos-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_NAME: "all"
|
||||
APP_ENV: "docker"
|
||||
AWS_REGION: us-east-1
|
||||
AWS_ACCESS_KEY_ID: minioadmin
|
||||
AWS_SECRET_ACCESS_KEY: minioadmin
|
||||
DB_STRATEGY: "migrate"
|
||||
BOOTSTRAP_CITRINEOS_DATABASE_HOST: "cariflex-citrineos-db"
|
||||
BOOTSTRAP_CITRINEOS_CONFIG_FILENAME: "config.json"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE: "local"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_FILE_PATH: "/data"
|
||||
CONFIG_CITRINEOS_WIPE_FILE_ON_START: "true"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
cariflex-amqp:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- citrineos-data:/data
|
||||
ports:
|
||||
- "8081:8080"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-citrineos-db:
|
||||
image: postgis/postgis:16-3.5
|
||||
container_name: cariflex-citrineos-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: citrine
|
||||
POSTGRES_USER: citrine
|
||||
POSTGRES_PASSWORD: citrine
|
||||
volumes:
|
||||
- citrineos-db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready --username=citrine
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-amqp:
|
||||
image: rabbitmq:3-management
|
||||
container_name: cariflex-amqp
|
||||
networks:
|
||||
cariflex-internal:
|
||||
aliases:
|
||||
- amqp-broker
|
||||
traefik-public:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.rabbitmq.rule=Host(`amqp.digitribe.fr`)"
|
||||
- "traefik.http.routers.rabbitmq.entrypoints=websecure"
|
||||
- "traefik.http.routers.rabbitmq.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.rabbitmq.loadbalancer.server.port=15672"
|
||||
volumes:
|
||||
- citrineos-amqp-data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: rabbitmq-diagnostics -q ping
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
hasura:
|
||||
image: hasura/graphql-engine:v2.40.0
|
||||
container_name: cariflex-hasura
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:8080"
|
||||
environment:
|
||||
HASURA_GRAPHQL_DATABASE_URL: "postgresql://citrine:***@cariflex-citrineos-db:5432/citrine"
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: "Digitribe972"
|
||||
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "anonymous"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.hasura.rule=Host(`hasura.digitribe.fr`)"
|
||||
- "traefik.http.routers.hasura.entrypoints=websecure"
|
||||
- "traefik.http.routers.hasura.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.hasura.loadbalancer.server.port=8080"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
citrineos-operator-ui:
|
||||
image: citrineos-core-main-citrine-ui:latest
|
||||
container_name: cariflex-citrineos-operator-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3002:3000"
|
||||
environment:
|
||||
NEXTAUTH_SECRET: Digitribe972
|
||||
ADMIN_PASSWORD: Digitribe972
|
||||
depends_on:
|
||||
- hasura
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.citrineos-ui.rule=Host(`citrineos.digitribe.fr`)"
|
||||
- "traefik.http.routers.citrineos-ui.entrypoints=websecure"
|
||||
- "traefik.http.routers.citrineos-ui.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.citrineos-ui.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
# === EVerest (simulateur de charge OCPP 2.0.1) ===
|
||||
everest-mqtt:
|
||||
image: ghcr.io/everest/everest-demo/mqtt-server:0.0.16
|
||||
container_name: cariflex-everest-mqtt
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
everest-nodered:
|
||||
image: ghcr.io/everest/everest-demo/nodered:0.0.16
|
||||
container_name: cariflex-everest-nodered
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- everest-mqtt
|
||||
environment:
|
||||
- MQTT_SERVER_ADDRESS=everest-mqtt
|
||||
- FLOWS=/config/config-sil-two-evse-flow.json
|
||||
networks:
|
||||
- cariflex-internal
|
||||
ports:
|
||||
- "1880:1880"
|
||||
|
||||
volumes:
|
||||
citrineos-data:
|
||||
driver: local
|
||||
citrineos-db-data:
|
||||
driver: local
|
||||
citrineos-amqp-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
cariflex-internal:
|
||||
name: config_cariflex-internal
|
||||
external: true
|
||||
171
snapshots/20260615_final/config/docker-compose-citrineos.yml
Normal file
171
snapshots/20260615_final/config/docker-compose-citrineos.yml
Normal file
@@ -0,0 +1,171 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
citrineos-server:
|
||||
image: ghcr.io/citrineos/citrineos-server:latest
|
||||
container_name: cariflex-citrineos-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
APP_NAME: "all"
|
||||
APP_ENV: "docker"
|
||||
AWS_REGION: us-east-1
|
||||
AWS_ACCESS_KEY_ID: minioadmin
|
||||
AWS_SECRET_ACCESS_KEY: minioadmin
|
||||
DB_STRATEGY: "migrate"
|
||||
BOOTSTRAP_CITRINEOS_DATABASE_HOST: "cariflex-citrineos-db"
|
||||
BOOTSTRAP_CITRINEOS_CONFIG_FILENAME: "config.json"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_TYPE: "local"
|
||||
BOOTSTRAP_CITRINEOS_FILE_ACCESS_LOCAL_FILE_PATH: "/data"
|
||||
CONFIG_CITRINEOS_WIPE_FILE_ON_START: "true"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
cariflex-amqp:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- citrineos-data:/data
|
||||
ports:
|
||||
- "8081:8080"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-citrineos-db:
|
||||
image: postgis/postgis:16-3.5
|
||||
container_name: cariflex-citrineos-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: citrine
|
||||
POSTGRES_USER: citrine
|
||||
POSTGRES_PASSWORD: citrine
|
||||
volumes:
|
||||
- citrineos-db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready --username=citrine
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
cariflex-amqp:
|
||||
image: rabbitmq:3-management
|
||||
container_name: cariflex-amqp
|
||||
networks:
|
||||
cariflex-internal:
|
||||
aliases:
|
||||
- amqp-broker
|
||||
traefik-public:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.rabbitmq.rule=Host(`amqp.digitribe.fr`)"
|
||||
- "traefik.http.routers.rabbitmq.entrypoints=websecure"
|
||||
- "traefik.http.routers.rabbitmq.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.rabbitmq.loadbalancer.server.port=15672"
|
||||
volumes:
|
||||
- citrineos-amqp-data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: rabbitmq-diagnostics -q ping
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
hasura:
|
||||
image: hasura/graphql-engine:v2.40.0
|
||||
container_name: cariflex-hasura
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8082:8080"
|
||||
environment:
|
||||
HASURA_GRAPHQL_DATABASE_URL: "postgresql://citrine:citrine@cariflex-citrineos-db:5432/citrine"
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: "Digitribe972"
|
||||
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "anonymous"
|
||||
depends_on:
|
||||
cariflex-citrineos-db:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.hasura.rule=Host(`hasura.digitribe.fr`)"
|
||||
- "traefik.http.routers.hasura.entrypoints=websecure"
|
||||
- "traefik.http.routers.hasura.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.hasura.loadbalancer.server.port=8080"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
citrineos-operator-ui:
|
||||
image: citrineos-core-main-citrine-ui:latest
|
||||
container_name: cariflex-citrineos-operator-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3002:3000"
|
||||
environment:
|
||||
NEXTAUTH_SECRET: Digitribe972
|
||||
ADMIN_PASSWORD: Digitribe972
|
||||
depends_on:
|
||||
- hasura
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.citrineos-ui.rule=Host(`citrineos.digitribe.fr`)"
|
||||
- "traefik.http.routers.citrineos-ui.entrypoints=websecure"
|
||||
- "traefik.http.routers.citrineos-ui.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.citrineos-ui.loadbalancer.server.port=3000"
|
||||
networks:
|
||||
- traefik-public
|
||||
- cariflex-internal
|
||||
|
||||
# === EVerest MQTT + NodeRED (UI de contrôle) ===
|
||||
everest-mqtt:
|
||||
image: ghcr.io/everest/everest-demo/mqtt-server:0.0.16
|
||||
container_name: cariflex-everest-mqtt
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
everest-nodered:
|
||||
image: ghcr.io/everest/everest-demo/nodered:0.0.16
|
||||
container_name: cariflex-everest-nodered
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- everest-mqtt
|
||||
environment:
|
||||
- MQTT_SERVER_ADDRESS=everest-mqtt
|
||||
- FLOWS=/config/config-sil-two-evse-flow.json
|
||||
networks:
|
||||
- cariflex-internal
|
||||
ports:
|
||||
- "1880:1880"
|
||||
|
||||
# === OCPP 2.0.1 Simulators ===
|
||||
ocpp-simulator:
|
||||
build:
|
||||
context: /home/eric/cariflex/scripts
|
||||
dockerfile: Dockerfile.simulator
|
||||
container_name: cariflex-ocpp-simulator
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
OCPP_HOST: "cariflex-citrineos-server"
|
||||
OCPP_PORT: "8082"
|
||||
networks:
|
||||
- cariflex-internal
|
||||
|
||||
volumes:
|
||||
citrineos-data:
|
||||
driver: local
|
||||
citrineos-db-data:
|
||||
driver: local
|
||||
citrineos-amqp-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
cariflex-internal:
|
||||
name: config_cariflex-internal
|
||||
external: true
|
||||
62
snapshots/20260615_final/containers.txt
Normal file
62
snapshots/20260615_final/containers.txt
Normal file
@@ -0,0 +1,62 @@
|
||||
cariflex-citrineos-operator-ui citrineos-core-main-citrine-ui:latest Up 5 minutes
|
||||
cariflex-everest-nodered ghcr.io/everest/everest-demo/nodered:0.0.16 Up 2 minutes (healthy)
|
||||
cariflex-hasura hasura/graphql-engine:v2.40.0 Up 6 hours (healthy)
|
||||
cariflex-ocpp-simulator config-ocpp-simulator Up 7 hours
|
||||
f33f58046fca_cariflex-amqp rabbitmq:3-management Up 7 hours (healthy)
|
||||
cariflex-everest-manager everest-manager:latest Restarting (1) 38 seconds ago
|
||||
cariflex-everest-mqtt ghcr.io/everest/everest-demo/mqtt-server:0.0.16 Up 19 hours
|
||||
cariflex-citrineos-server ghcr.io/citrineos/citrineos-server:latest Up 4 days
|
||||
cariflex-citrineos-db postgis/postgis:16-3.5 Up 4 days (healthy)
|
||||
phpipam-phpipam-web-1 phpipam/phpipam-www:latest Up 5 days
|
||||
phpipam-phpipam-cron-1 phpipam/phpipam-cron:latest Up 5 days
|
||||
phpipam-phpipam-mariadb-1 mariadb:latest Up 5 days
|
||||
openadr-ven flexmeasures-openadr-ven Up 5 days
|
||||
openadr-vtn flexmeasures-openadr-vtn Up 5 days
|
||||
flexmeasures-redis redis:7-alpine Up 6 days
|
||||
flexmeasures-worker lfenergy/flexmeasures:latest Up 6 days
|
||||
flexmeasures-server lfenergy/flexmeasures:latest Up 5 days
|
||||
smart-city-grafana grafana/grafana:11.3.0 Up 5 days
|
||||
flexmeasures-db postgres:17 Up 7 days
|
||||
smart-city-simulator smart-city-digital-twin-martinique_simulator Up 8 days
|
||||
starrocks-be starrocks/be-ubuntu:3.3.5 Up 8 days
|
||||
starrocks-fe starrocks/fe-ubuntu:3.3.5 Up 8 days (healthy)
|
||||
smart-city-orion-ld fiware/orion-ld:latest Up 8 days (healthy)
|
||||
emqx_emqx_1 emqx/emqx:5.4 Up 8 days
|
||||
smart-city-chirpstack-rest-api-1 chirpstack/chirpstack-rest-api:4 Up 8 days
|
||||
smart-city-chirpstack-1 chirpstack/chirpstack:latest Up 8 days
|
||||
smart-city-chirpstack-gateway-bridge-1 chirpstack/chirpstack-gateway-bridge:4 Up 8 days
|
||||
smart-city-mosquitto-1 eclipse-mosquitto:2 Up 8 days
|
||||
smart-city-redis-1 redis:7-alpine Up 8 days
|
||||
smart-city-postgres-1 postgres:14-alpine Up 8 days
|
||||
bunkerm-bunkerm-1 bunkeriot/bunkerm:latest Up 8 days (healthy)
|
||||
metabase-app metabase/metabase:latest Up 8 days (healthy)
|
||||
smart-city-iot-agent-mosquitto fiware/iotagent-json:latest Up 8 days (healthy)
|
||||
smart-city-redpanda-consumer python:3.11-slim Up 8 days (unhealthy)
|
||||
smart-city-geojson-proxy smart-city-geojson-proxy Up 8 days
|
||||
smart-city-loki grafana/loki:latest Up 8 days
|
||||
smart-city-telegraf telegraf:1.28 Up 8 days
|
||||
smart-city-iot-agent-emqx fiware/iotagent-json:latest Up 8 days (healthy)
|
||||
smart-city-tts-redis redis:7 Up 8 days
|
||||
smart-city-cratedb crate:5.5 Up 8 days (healthy)
|
||||
metabase-postgres postgres:15-alpine Up 8 days (healthy)
|
||||
smart-city-iot-mongodb mongo:4.4 Up 8 days (healthy)
|
||||
smart-city-tts-postgres postgres:14 Up 8 days
|
||||
smart-city-influxdb influxdb:2.7-alpine Up 8 days (healthy)
|
||||
smart-city-promtail grafana/promtail:latest Up 8 days
|
||||
smart-city-kepler nginx:alpine Up 8 days
|
||||
kepler-backend crazycapivara/kepler.gl:latest Up 9 days
|
||||
trino trinodb/trino:450 Up 9 days (healthy)
|
||||
trino-nginx nginx:alpine Up 10 days
|
||||
streamlit python:3.11-slim Up 10 days
|
||||
delta-lake delta-lake:local Up 10 days
|
||||
clickhouse clickhouse/clickhouse-server:24.5 Up 10 days
|
||||
duckdb duckdb-api:local Up 10 days
|
||||
test-backend nginx:alpine Up 10 days
|
||||
airflow-scheduler apache/airflow:2.9.3-python3.11 Up 12 days (healthy)
|
||||
airflow-webserver apache/airflow:2.9.3-python3.11 Up 12 days (healthy)
|
||||
airflow-postgres postgres:16 Up 12 days (healthy)
|
||||
smartapp-api smartapp-api:latest Up 13 days
|
||||
smartapp-web nginx:alpine Up 13 days
|
||||
gitea-runner gitea/act_runner:latest Up 13 days
|
||||
traefik traefik:v3.1 Up 16 minutes
|
||||
gitea gitea/gitea:latest Up 13 days
|
||||
1
snapshots/20260615_final/hasura-metadata.json
Normal file
1
snapshots/20260615_final/hasura-metadata.json
Normal file
File diff suppressed because one or more lines are too long
29
snapshots/20260615_final/scripts/configure-auth.py
Normal file
29
snapshots/20260615_final/scripts/configure-auth.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Configure BasicAuthPassword for all 15 Cariflex charging stations"""
|
||||
import json, time, urllib.request, urllib.error
|
||||
|
||||
CITRINEOS_URL = "http://localhost:8081"
|
||||
PASSWORD = "DEADBEEFDEADBEEF"
|
||||
|
||||
for i in range(1, 16):
|
||||
cp_id = f"CP{i:03d}"
|
||||
url = f"{CITRINEOS_URL}/data/monitoring/variableAttribute?stationId={cp_id}&setOnCharger=true"
|
||||
payload = json.dumps({
|
||||
"component": {"name": "SecurityCtrlr"},
|
||||
"variable": {"name": "BasicAuthPassword"},
|
||||
"variableAttribute": [{"value": PASSWORD}],
|
||||
"variableCharacteristics": {"dataType": "passwordString", "supportsMonitoring": False}
|
||||
}).encode()
|
||||
|
||||
req = urllib.request.Request(url, data=payload, method='PUT',
|
||||
headers={'Content-Type': 'application/json'})
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
print(f"OK {cp_id}: HTTP {resp.status}")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"FAIL {cp_id}: HTTP {e.code}")
|
||||
except Exception as e:
|
||||
print(f"FAIL {cp_id}: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
print("Done")
|
||||
21
snapshots/20260615_final/traefik-nodered.yml
Normal file
21
snapshots/20260615_final/traefik-nodered.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
http:
|
||||
routers:
|
||||
nodered:
|
||||
rule: "Host(`nodered.digitribe.fr`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: nodered
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
nodered-http:
|
||||
rule: "Host(`nodered.digitribe.fr`)"
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- redirect-https
|
||||
service: nodered
|
||||
services:
|
||||
nodered:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://172.29.0.39:1880"
|
||||
12
tools/citrineos-core-main/Dockerfile.ui
Normal file
12
tools/citrineos-core-main/Dockerfile.ui
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:24.16.0-alpine
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
||||
COPY --chown=nextjs:nodejs apps/operator-ui/.next/standalone ./
|
||||
COPY --chown=nextjs:nodejs apps/operator-ui/.next/static ./apps/operator-ui/.next/static
|
||||
COPY --chown=nextjs:nodejs apps/operator-ui/public ./apps/operator-ui/public
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "apps/operator-ui/server.js"]
|
||||
@@ -40,7 +40,7 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
webpack: (config) => {
|
||||
webpack: (config, { isServer }) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'class-transformer/types/storage': resolve(
|
||||
@@ -48,8 +48,35 @@ const nextConfig = {
|
||||
'node_modules/class-transformer/cjs/storage.js',
|
||||
),
|
||||
};
|
||||
// Force new file names to bust cache
|
||||
if (!isServer) {
|
||||
config.output.filename = 'static/chunks/[name].[contenthash:8].js';
|
||||
config.output.chunkFilename = 'static/chunks/[name].[contenthash:8].js';
|
||||
}
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'no-referrer-when-downgrade',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/_next/static/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'no-cache, no-store, must-revalidate',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
@@ -10,5 +10,5 @@ type PageProps = {
|
||||
|
||||
export default async function ShowChargingStationPage({ params }: PageProps) {
|
||||
const { id } = await params;
|
||||
return <ChargingStationDetail params={{ id: Number(id) }} />;
|
||||
return <ChargingStationDetail params={{ id: String(id) }} />;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,8 @@ export const ChargingStationDetailCard = ({
|
||||
queryOptions: getPlainToInstanceOptions(ChargingStationClass, true),
|
||||
});
|
||||
|
||||
const station = data?.data;
|
||||
const stationData = data as any;
|
||||
const station = stationData?.data?.ChargingStations?.[0] || stationData?.data || null;
|
||||
|
||||
const {
|
||||
query: { data: latestLogsData },
|
||||
@@ -89,9 +90,9 @@ export const ChargingStationDetailCard = ({
|
||||
sorters: [{ field: OCPPMessageProps.timestamp, order: 'desc' }],
|
||||
filters: [
|
||||
{
|
||||
field: OCPPMessageProps.ocppConnectionName,
|
||||
field: 'stationId',
|
||||
operator: 'eq',
|
||||
value: station?.ocppConnectionName,
|
||||
value: station?.id,
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// CACHE_BUST: 2026-06-16-08-30-00
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -10,27 +11,23 @@ import { ChargingStationDetailCard } from '@lib/client/pages/charging-stations/d
|
||||
import { pageFlex, pageMargin } from '@lib/client/styles/page';
|
||||
import { ChargingStationDetailTabsCard } from '@lib/client/pages/charging-stations/detail/charging.station.detail.tabs.card';
|
||||
import { S3_BUCKET_FOLDER_IMAGES_CHARGING_STATIONS } from '@lib/utils/consts';
|
||||
import { getPresignedUrlForGet } from '@lib/server/actions/file/getPresingedUrlForGet';
|
||||
// import { getPresignedUrlForGet } from '@lib/server/actions/file/getPresingedUrlForGet';
|
||||
import { AccessDeniedFallbackCard } from '@lib/client/components/access-denied-fallback-card';
|
||||
import { Skeleton } from '@lib/client/components/ui/skeleton';
|
||||
|
||||
type ChargingStationDetailProps = {
|
||||
params: { id: number };
|
||||
params: { id: string };
|
||||
};
|
||||
|
||||
export const ChargingStationDetail: React.FC<ChargingStationDetailProps> = ({ params }) => {
|
||||
const { id } = params;
|
||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||
const [forceUpdate] = useState(Date.now()); // Force cache bust
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
getPresignedUrlForGet(`${S3_BUCKET_FOLDER_IMAGES_CHARGING_STATIONS}/${id}`).then((result) => {
|
||||
if (result.success) {
|
||||
return setImageUrl(result.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [id]);
|
||||
// S3 image loading disabled - no bucket configured
|
||||
void forceUpdate; // Used to force re-render
|
||||
}, [id, forceUpdate]);
|
||||
|
||||
if (!id) {
|
||||
return (
|
||||
|
||||
@@ -28,11 +28,12 @@ type LocationDetailProps = {
|
||||
|
||||
export const LocationsDetail = ({ params }: LocationDetailProps) => {
|
||||
const { id } = params;
|
||||
const numericId = parseInt(id, 10);
|
||||
const {
|
||||
query: { data, isLoading },
|
||||
} = useOne<LocationDto>({
|
||||
resource: ResourceType.LOCATIONS,
|
||||
id,
|
||||
id: numericId,
|
||||
meta: {
|
||||
gqlQuery: LOCATIONS_GET_QUERY,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ export const CHARGING_STATIONS_LIST_QUERY = gql`
|
||||
floorLevel
|
||||
parkingRestrictions
|
||||
capabilities
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -37,20 +37,20 @@ export const CHARGING_STATIONS_LIST_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
evses: Evses {
|
||||
evses: evses {
|
||||
id
|
||||
evseTypeId
|
||||
evseId
|
||||
physicalReference
|
||||
}
|
||||
LatestStatusNotifications {
|
||||
latestStatusNotifications {
|
||||
id
|
||||
stationId
|
||||
ocppConnectionName
|
||||
statusNotificationId
|
||||
updatedAt
|
||||
createdAt
|
||||
StatusNotification {
|
||||
statusNotification {
|
||||
connectorId
|
||||
connectorStatus
|
||||
createdAt
|
||||
@@ -62,7 +62,7 @@ export const CHARGING_STATIONS_LIST_QUERY = gql`
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
transactions: Transactions(where: { isActive: { _eq: true } }) {
|
||||
transactions: transactions(where: { isActive: { _eq: true } }) {
|
||||
id
|
||||
timeSpentCharging
|
||||
isActive
|
||||
@@ -77,7 +77,7 @@ export const CHARGING_STATIONS_LIST_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
connectors: Connectors {
|
||||
connectors: connectors {
|
||||
connectorId
|
||||
status
|
||||
errorCode
|
||||
@@ -126,7 +126,7 @@ export const FAULTED_CHARGING_STATIONS_LIST_QUERY = gql`
|
||||
locationId
|
||||
createdAt
|
||||
updatedAt
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -138,13 +138,13 @@ export const FAULTED_CHARGING_STATIONS_LIST_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
LatestStatusNotifications {
|
||||
latestStatusNotifications {
|
||||
id
|
||||
ocppConnectionName
|
||||
statusNotificationId
|
||||
updatedAt
|
||||
createdAt
|
||||
StatusNotification {
|
||||
statusNotification {
|
||||
connectorId
|
||||
connectorStatus
|
||||
createdAt
|
||||
@@ -193,8 +193,8 @@ export const CHARGING_STATIONS_STATUS_COUNT_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||
query GetChargingStationById($id: Int!) {
|
||||
ChargingStations_by_pk(id: $id) {
|
||||
query GetChargingStationById($id: String!) {
|
||||
ChargingStations(where: {id: {_eq: $id}}) {
|
||||
id
|
||||
tenantId
|
||||
ocppConnectionName
|
||||
@@ -211,7 +211,7 @@ export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||
capabilities
|
||||
coordinates
|
||||
use16StatusNotification0
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -223,9 +223,8 @@ export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
evses: Evses {
|
||||
evses: evses {
|
||||
id
|
||||
ocppConnectionName
|
||||
stationId
|
||||
evseTypeId
|
||||
evseId
|
||||
@@ -233,9 +232,8 @@ export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||
removed
|
||||
createdAt
|
||||
updatedAt
|
||||
connectors: Connectors {
|
||||
connectors: connectors {
|
||||
id
|
||||
ocppConnectionName
|
||||
evseId
|
||||
evseTypeConnectorId
|
||||
connectorId
|
||||
@@ -257,33 +255,29 @@ export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
LatestStatusNotifications {
|
||||
latestStatusNotifications {
|
||||
id
|
||||
ocppConnectionName
|
||||
stationId
|
||||
statusNotificationId
|
||||
updatedAt
|
||||
createdAt
|
||||
StatusNotification {
|
||||
statusNotification {
|
||||
connectorId
|
||||
connectorStatus
|
||||
createdAt
|
||||
evseId
|
||||
ocppConnectionName
|
||||
id
|
||||
stationId
|
||||
timestamp
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
transactions: Transactions(where: { isActive: { _eq: true } }) {
|
||||
transactions: transactions(where: { isActive: { _eq: true } }) {
|
||||
id
|
||||
stationId
|
||||
ocppConnectionName
|
||||
timeSpentCharging
|
||||
isActive
|
||||
chargingState
|
||||
ocppConnectionName
|
||||
stoppedReason
|
||||
transactionId
|
||||
evseId
|
||||
@@ -292,9 +286,8 @@ export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
connectors: Connectors {
|
||||
connectors: connectors {
|
||||
id
|
||||
ocppConnectionName
|
||||
stationId
|
||||
evseId
|
||||
connectorId
|
||||
@@ -320,7 +313,7 @@ export const CHARGING_STATIONS_GET_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const GET_CHARGING_STATIONS_WITH_LOCATION_AND_LATEST_STATUS_NOTIFICATIONS_AND_TRANSACTIONS = gql`
|
||||
query GetChargingStationsWithLocationAndLatestStatusNotificationsAndTransactions {
|
||||
query GetChargingStationsWithLocationAndLatestStatusNotificationsAndtransactions {
|
||||
ChargingStations {
|
||||
id
|
||||
ocppConnectionName
|
||||
@@ -329,8 +322,8 @@ export const GET_CHARGING_STATIONS_WITH_LOCATION_AND_LATEST_STATUS_NOTIFICATIONS
|
||||
locationId
|
||||
createdAt
|
||||
updatedAt
|
||||
latestStatusNotifications: LatestStatusNotifications {
|
||||
statusNotification: StatusNotification {
|
||||
latestStatusNotifications: latestStatusNotifications {
|
||||
statusNotification: statusNotification {
|
||||
id
|
||||
ocppConnectionName
|
||||
evseId
|
||||
@@ -341,12 +334,11 @@ export const GET_CHARGING_STATIONS_WITH_LOCATION_AND_LATEST_STATUS_NOTIFICATIONS
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
transactions: Transactions(where: { isActive: { _eq: true } }) {
|
||||
transactions: transactions(where: { isActive: { _eq: true } }) {
|
||||
id
|
||||
timeSpentCharging
|
||||
isActive
|
||||
chargingState
|
||||
ocppConnectionName
|
||||
stoppedReason
|
||||
transactionId
|
||||
evseId
|
||||
@@ -355,7 +347,7 @@ export const GET_CHARGING_STATIONS_WITH_LOCATION_AND_LATEST_STATUS_NOTIFICATIONS
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -367,7 +359,7 @@ export const GET_CHARGING_STATIONS_WITH_LOCATION_AND_LATEST_STATUS_NOTIFICATIONS
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
evses: Evses {
|
||||
evses: evses {
|
||||
id
|
||||
evseTypeId
|
||||
createdAt
|
||||
|
||||
@@ -32,20 +32,20 @@ export const LOCATIONS_LIST_QUERY = gql`
|
||||
protocol
|
||||
createdAt
|
||||
updatedAt
|
||||
evses: Evses {
|
||||
evses: evses {
|
||||
id
|
||||
evseTypeId
|
||||
evseId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
LatestStatusNotifications {
|
||||
latestStatusNotifications {
|
||||
id
|
||||
ocppConnectionName
|
||||
statusNotificationId
|
||||
updatedAt
|
||||
createdAt
|
||||
StatusNotification {
|
||||
statusNotification {
|
||||
connectorId
|
||||
connectorStatus
|
||||
createdAt
|
||||
@@ -56,7 +56,7 @@ export const LOCATIONS_LIST_QUERY = gql`
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
transactions: Transactions(where: { isActive: { _eq: true } }) {
|
||||
transactions: transactions(where: { isActive: { _eq: true } }) {
|
||||
id
|
||||
timeSpentCharging
|
||||
isActive
|
||||
@@ -70,7 +70,7 @@ export const LOCATIONS_LIST_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
connectors: Connectors {
|
||||
connectors: connectors {
|
||||
connectorId
|
||||
status
|
||||
errorCode
|
||||
@@ -123,13 +123,13 @@ export const LOCATIONS_GET_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
LatestStatusNotifications {
|
||||
latestStatusNotifications {
|
||||
id
|
||||
ocppConnectionName
|
||||
statusNotificationId
|
||||
updatedAt
|
||||
createdAt
|
||||
StatusNotification {
|
||||
statusNotification {
|
||||
connectorId
|
||||
connectorStatus
|
||||
createdAt
|
||||
@@ -140,7 +140,7 @@ export const LOCATIONS_GET_QUERY = gql`
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
Transactions(where: { isActive: { _eq: true } }) {
|
||||
transactions(where: { isActive: { _eq: true } }) {
|
||||
id
|
||||
timeSpentCharging
|
||||
isActive
|
||||
|
||||
@@ -26,7 +26,7 @@ export const TRANSACTION_LIST_QUERY = gql`
|
||||
endTime
|
||||
createdAt
|
||||
updatedAt
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -38,19 +38,19 @@ export const TRANSACTION_LIST_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
evse: Evse {
|
||||
evse: evse {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
connector: Connector {
|
||||
connector: connector {
|
||||
id
|
||||
connectorId
|
||||
type
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
authorization: Authorization {
|
||||
authorization: authorization {
|
||||
id
|
||||
idToken
|
||||
idTokenType
|
||||
@@ -66,7 +66,7 @@ export const TRANSACTION_LIST_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
chargingStation: ChargingStation {
|
||||
chargingStation: chargingStation {
|
||||
id
|
||||
ocppConnectionName
|
||||
isOnline
|
||||
@@ -74,7 +74,7 @@ export const TRANSACTION_LIST_QUERY = gql`
|
||||
locationId
|
||||
createdAt
|
||||
updatedAt
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -124,7 +124,7 @@ export const GET_TRANSACTIONS_FOR_AUTHORIZATION = gql`
|
||||
endTime
|
||||
createdAt
|
||||
updatedAt
|
||||
chargingStation: ChargingStation {
|
||||
chargingStation: chargingStation {
|
||||
id
|
||||
ocppConnectionName
|
||||
isOnline
|
||||
@@ -132,7 +132,7 @@ export const GET_TRANSACTIONS_FOR_AUTHORIZATION = gql`
|
||||
locationId
|
||||
createdAt
|
||||
updatedAt
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -199,17 +199,17 @@ export const GET_TRANSACTION_LIST_FOR_STATION = gql`
|
||||
StartTransaction {
|
||||
idTokenDatabaseId
|
||||
}
|
||||
authorization: Authorization {
|
||||
authorization: authorization {
|
||||
id
|
||||
idToken
|
||||
}
|
||||
chargingStation: ChargingStation {
|
||||
chargingStation: chargingStation {
|
||||
id
|
||||
isOnline
|
||||
locationId
|
||||
createdAt
|
||||
updatedAt
|
||||
location: Location {
|
||||
location: location {
|
||||
id
|
||||
name
|
||||
address
|
||||
@@ -267,7 +267,7 @@ export const TRANSACTION_GET_QUERY = gql`
|
||||
endTime
|
||||
createdAt
|
||||
updatedAt
|
||||
location: Location {
|
||||
location: location {
|
||||
name
|
||||
address
|
||||
city
|
||||
@@ -278,14 +278,14 @@ export const TRANSACTION_GET_QUERY = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
evse: Evse {
|
||||
evse: evse {
|
||||
id
|
||||
evseTypeId
|
||||
evseId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
connector: Connector {
|
||||
connector: connector {
|
||||
id
|
||||
connectorId
|
||||
type
|
||||
@@ -297,7 +297,7 @@ export const TRANSACTION_GET_QUERY = gql`
|
||||
pricePerKwh
|
||||
}
|
||||
}
|
||||
authorization: Authorization {
|
||||
authorization: authorization {
|
||||
id
|
||||
idToken
|
||||
idTokenType
|
||||
|
||||
@@ -38,7 +38,33 @@ export async function getPlaceDetails(
|
||||
throw new Error('Place ID is required');
|
||||
}
|
||||
|
||||
// Return stub data - Google Places API removed, using OSM/Nominatim instead
|
||||
try {
|
||||
// Use Nominatim OSM for place details (free, no API key)
|
||||
const url = `https://nominatim.openstreetmap.org/lookup?format=json&osm_ids=${placeId}&addressdetails=1`;
|
||||
const response = await fetch(url, {
|
||||
headers: { 'User-Agent': 'CitrineOS-Operator-UI/1.0' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Nominatim error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data && data.length > 0) {
|
||||
const place = data[0];
|
||||
return {
|
||||
id: place.place_id?.toString() || placeId,
|
||||
displayName: { text: place.display_name || '', languageCode: 'en' },
|
||||
formattedAddress: place.display_name || '',
|
||||
addressComponents: [],
|
||||
location: {
|
||||
latitude: parseFloat(place.lat) || 0,
|
||||
longitude: parseFloat(place.lon) || 0,
|
||||
},
|
||||
types: place.type ? [place.type] : [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: placeId,
|
||||
displayName: { text: '', languageCode: 'en' },
|
||||
@@ -46,5 +72,15 @@ export async function getPlaceDetails(
|
||||
addressComponents: [],
|
||||
location: { latitude: 0, longitude: 0 },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('getPlaceDetails error:', error);
|
||||
return {
|
||||
id: placeId,
|
||||
displayName: { text: '', languageCode: 'en' },
|
||||
formattedAddress: '',
|
||||
addressComponents: [],
|
||||
location: { latitude: 0, longitude: 0 },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
75
tools/citrineos-core-main/pnpm-lock.yaml
generated
75
tools/citrineos-core-main/pnpm-lock.yaml
generated
@@ -81,7 +81,7 @@ importers:
|
||||
version: 6.6.5
|
||||
sequelize-typescript:
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8)
|
||||
version: 2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8(sqlite3@5.1.7))
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -194,7 +194,7 @@ importers:
|
||||
version: 6.37.8(pg-hstore@2.3.4)(pg@8.11.3)(sqlite3@5.1.7)
|
||||
sequelize-typescript:
|
||||
specifier: 2.1.6
|
||||
version: 2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8)
|
||||
version: 2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8(sqlite3@5.1.7))
|
||||
ts-node:
|
||||
specifier: 10.9.2
|
||||
version: 10.9.2(@types/node@25.9.1)(typescript@6.0.3)
|
||||
@@ -319,6 +319,9 @@ importers:
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@19.1.4(react@19.1.4))(react@19.1.4)
|
||||
'@types/leaflet':
|
||||
specifier: ^1.9.21
|
||||
version: 1.9.21
|
||||
'@vis.gl/react-google-maps':
|
||||
specifier: 1.5.1
|
||||
version: 1.5.1(react-dom@19.1.4(react@19.1.4))(react@19.1.4)
|
||||
@@ -364,6 +367,9 @@ importers:
|
||||
js-cookie:
|
||||
specifier: 3.0.5
|
||||
version: 3.0.5
|
||||
leaflet:
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4
|
||||
lodash:
|
||||
specifier: ^4.18.1
|
||||
version: 4.18.1
|
||||
@@ -406,6 +412,9 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: 7.65.0
|
||||
version: 7.65.0(react@19.1.4)
|
||||
react-leaflet:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(leaflet@1.9.4)(react-dom@19.1.4(react@19.1.4))(react@19.1.4)
|
||||
react-redux:
|
||||
specifier: 9.2.0
|
||||
version: 9.2.0(@types/react@19.1.4)(react@19.1.4)(redux@5.0.1)
|
||||
@@ -698,7 +707,7 @@ importers:
|
||||
version: 6.37.8(pg-hstore@2.3.4)(pg@8.11.3)(sqlite3@5.1.7)
|
||||
sequelize-typescript:
|
||||
specifier: 2.1.6
|
||||
version: 2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8)
|
||||
version: 2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8(sqlite3@5.1.7))
|
||||
ssh2-sftp-client:
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3
|
||||
@@ -3304,6 +3313,13 @@ packages:
|
||||
'@reach/observe-rect@1.2.0':
|
||||
resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==}
|
||||
|
||||
'@react-leaflet/core@3.0.0':
|
||||
resolution: {integrity: sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==}
|
||||
peerDependencies:
|
||||
leaflet: ^1.9.0
|
||||
react: ^19.0.0
|
||||
react-dom: ^19.0.0
|
||||
|
||||
'@redis/bloom@1.2.0':
|
||||
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
|
||||
peerDependencies:
|
||||
@@ -3940,6 +3956,9 @@ packages:
|
||||
'@types/jsrsasign@10.5.15':
|
||||
resolution: {integrity: sha512-3stUTaSRtN09PPzVWR6aySD9gNnuymz+WviNHoTb85dKu+BjaV4uBbWWGykBBJkfwPtcNZVfTn2lbX00U+yhpQ==}
|
||||
|
||||
'@types/leaflet@1.9.21':
|
||||
resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==}
|
||||
|
||||
'@types/lodash.debounce@4.0.9':
|
||||
resolution: {integrity: sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==}
|
||||
|
||||
@@ -6987,6 +7006,9 @@ packages:
|
||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
engines: {node: '>= 0.6.3'}
|
||||
|
||||
leaflet@1.9.4:
|
||||
resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==}
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -8214,6 +8236,13 @@ packages:
|
||||
react-is@19.1.4:
|
||||
resolution: {integrity: sha512-4oz30Hl3G6Ji4MpJEMwfXCjsfjAarc9nUPLmSXJLDjHbQRTeXMgf8UAPcrsUYVV+7E7mPHqNfhi5KVVMhl+zcg==}
|
||||
|
||||
react-leaflet@5.0.0:
|
||||
resolution: {integrity: sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==}
|
||||
peerDependencies:
|
||||
leaflet: ^1.9.0
|
||||
react: ^19.0.0
|
||||
react-dom: ^19.0.0
|
||||
|
||||
react-reconciler@0.29.2:
|
||||
resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -12262,6 +12291,12 @@ snapshots:
|
||||
|
||||
'@reach/observe-rect@1.2.0': {}
|
||||
|
||||
'@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.1.4(react@19.1.4))(react@19.1.4)':
|
||||
dependencies:
|
||||
leaflet: 1.9.4
|
||||
react: 19.1.4
|
||||
react-dom: 19.1.4(react@19.1.4)
|
||||
|
||||
'@redis/bloom@1.2.0(@redis/client@1.6.1)':
|
||||
dependencies:
|
||||
'@redis/client': 1.6.1
|
||||
@@ -12920,6 +12955,10 @@ snapshots:
|
||||
|
||||
'@types/jsrsasign@10.5.15': {}
|
||||
|
||||
'@types/leaflet@1.9.21':
|
||||
dependencies:
|
||||
'@types/geojson': 7946.0.16
|
||||
|
||||
'@types/lodash.debounce@4.0.9':
|
||||
dependencies:
|
||||
'@types/lodash': 4.17.24
|
||||
@@ -14934,8 +14973,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
eslint: 9.16.0(jiti@2.7.0)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0)))(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.16.0(jiti@2.7.0))
|
||||
@@ -14958,7 +14997,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0)))(eslint@9.16.0(jiti@2.7.0)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.16.0(jiti@2.7.0)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
@@ -14969,22 +15008,21 @@ snapshots:
|
||||
tinyglobby: 0.2.17
|
||||
unrs-resolver: 1.12.2
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0)))(eslint@9.16.0(jiti@2.7.0)))(eslint@9.16.0(jiti@2.7.0)):
|
||||
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@9.16.0(jiti@2.7.0)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
'@typescript-eslint/parser': 8.17.0(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
eslint: 9.16.0(jiti@2.7.0)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0)))(eslint@9.16.0(jiti@2.7.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.16.0(jiti@2.7.0)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -14995,7 +15033,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.16.0(jiti@2.7.0)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.16.0(jiti@2.7.0)))(eslint@9.16.0(jiti@2.7.0)))(eslint@9.16.0(jiti@2.7.0))
|
||||
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.17.0(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@9.16.0(jiti@2.7.0))
|
||||
hasown: 2.0.4
|
||||
is-core-module: 2.16.2
|
||||
is-glob: 4.0.3
|
||||
@@ -15007,7 +15045,7 @@ snapshots:
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.60.1(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
'@typescript-eslint/parser': 8.17.0(eslint@9.16.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
@@ -16487,6 +16525,8 @@ snapshots:
|
||||
dependencies:
|
||||
readable-stream: 2.3.8
|
||||
|
||||
leaflet@1.9.4: {}
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -17761,6 +17801,13 @@ snapshots:
|
||||
|
||||
react-is@19.1.4: {}
|
||||
|
||||
react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.1.4(react@19.1.4))(react@19.1.4):
|
||||
dependencies:
|
||||
'@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.1.4(react@19.1.4))(react@19.1.4)
|
||||
leaflet: 1.9.4
|
||||
react: 19.1.4
|
||||
react-dom: 19.1.4(react@19.1.4)
|
||||
|
||||
react-reconciler@0.29.2(react@18.3.1):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -18217,7 +18264,7 @@ snapshots:
|
||||
|
||||
sequelize-pool@7.1.0: {}
|
||||
|
||||
sequelize-typescript@2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8):
|
||||
sequelize-typescript@2.1.6(@types/node@25.9.1)(@types/validator@13.15.10)(reflect-metadata@0.2.2)(sequelize@6.37.8(sqlite3@5.1.7)):
|
||||
dependencies:
|
||||
'@types/node': 25.9.1
|
||||
'@types/validator': 13.15.10
|
||||
|
||||
Reference in New Issue
Block a user