fix: VariableAttributes mapping + locations detail parseInt + .gitignore pnpm-store

This commit is contained in:
Eric F
2026-06-15 22:28:31 -04:00
parent 438f9aa952
commit b06dbdbba9
30 changed files with 23411 additions and 0 deletions

2
.env.citrineos-ui Normal file
View File

@@ -0,0 +1,2 @@
NEXT_PUBLIC_AUTH_PROVIDER=***
N...**

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ tools/*/__pycache__
tools/*/.git
*.zip
tools/citrineos-core-main/src
tools/citrineos-core-main/.pnpm-store/

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View 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"}}

View 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

File diff suppressed because one or more lines are too long

View 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)

View 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

View 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

File diff suppressed because one or more lines are too long

View 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"]

View 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"]

View 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")

View 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 ==="

View 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); });

View 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();

View 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); });

File diff suppressed because one or more lines are too long

View 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

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

View 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")

View 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"