fix: VariableAttributes mapping + locations detail parseInt + .gitignore pnpm-store
This commit is contained in:
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
|
tools/*/.git
|
||||||
*.zip
|
*.zip
|
||||||
tools/citrineos-core-main/src
|
tools/citrineos-core-main/src
|
||||||
|
tools/citrineos-core-main/.pnpm-store/
|
||||||
|
|||||||
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.
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"
|
||||||
Reference in New Issue
Block a user