diff --git a/.gitignore b/.gitignore index c1492b1..45222a3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ etc/addons/ etc/sessions/ etc/filestore/ iac/.terraform -iac/.terraform.lock.hcl \ No newline at end of file +iac/.terraform.lock.hcl + +# Generated by setup.sh --edges N +openems-edge/config-edge*/ +docker-compose.override.yml diff --git a/docker-compose.yml b/docker-compose.yml index 3c23c1a..59441ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,17 +12,19 @@ services: context: ./openems-backend image: openems-backend:latest environment: - DB_HOST: applicationdb.clkigksc2ezs.us-east-1.rds.amazonaws.com # Assumes the PostgreSQL service is named openems-database + DB_HOST: db DB_PORT: "5432" - DB_NAME: openemsdb - DB_USER: openems - DB_PASSWORD: openemspassword + DB_NAME: openems + DB_USER: odoo + DB_PASSWORD: Icui4cyou ports: + - "8075:8075" # B2B REST API (meter discovery) - "8079:8079" - "8081:8081" - "8082:8082" - # depends_on: - # - openems-database + depends_on: + influxdb: + condition: service_healthy openems-edge: build: @@ -34,6 +36,28 @@ services: volumes: - ./openems-edge/config.d:/etc/openems.d + # InfluxDB — timedata persistence for the OpenEMS backend. + # Stores historical energy data (measurements, meter readings, etc.) + # in a named volume (influxdb-data) so data survives container restarts. + # + # Verify data is flowing: + # curl http://localhost:8086/query --data-urlencode "q=SHOW MEASUREMENTS ON openemsdb" + influxdb: + image: influxdb:1.8 + restart: unless-stopped + environment: + INFLUXDB_DB: openemsdb + INFLUXDB_HTTP_AUTH_ENABLED: "false" + ports: + - "127.0.0.1:8086:8086" + volumes: + - influxdb-data:/var/lib/influxdb + healthcheck: + test: ["CMD", "influx", "-execute", "SHOW DATABASES"] + interval: 10s + timeout: 5s + retries: 5 + db: build: context: ./odoo-database @@ -107,5 +131,5 @@ services: # POSTGRES_DB: odoodb # volumes: # - odoo-db-data:/var/lib/postgresql/data -# volumes: -# odoo-db-data: +volumes: + influxdb-data: diff --git a/openems-backend/Dockerfile b/openems-backend/Dockerfile index 2b0fb90..b5a92b0 100644 --- a/openems-backend/Dockerfile +++ b/openems-backend/Dockerfile @@ -6,13 +6,11 @@ COPY config.d ./config.d COPY openems-backend.sh ./ RUN apt-get update RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common curl gnupg -RUN curl -sL https://repos.influxdata.com/influxdb.key -o /etc/apt/trusted.gpg.d/influxdb.asc #RUN add-apt-repository -y ppa:openjdk-r/ppa RUN apt-get install -y openjdk-21-jdk RUN apt-get install -y openjdk-21-jre -RUN apt-get install -y influxdb #RUN update-alternatives --config java #RUN update-alternatives --config javac diff --git a/openems-backend/config.d/Metadata/Odoo.config b/openems-backend/config.d/Metadata/Odoo.config index b454c43..c808769 100644 --- a/openems-backend/config.d/Metadata/Odoo.config +++ b/openems-backend/config.d/Metadata/Odoo.config @@ -1,11 +1,10 @@ :org.apache.felix.configadmin.revision:=L"1" -database="odoodb" -pgHost="localhost" -pgPassword="openemspassword" -pgUser="openems" -odooHost="localhost" -odooPassword="admin" +database="openems" +pgHost="db" +pgPassword="Icui4cyou" +pgUser="odoo" +odooHost="odoo16" +odooPassword="Icui4cyou" odooPort=I"8069" service.pid="Metadata.Odoo" -odooPassword="Icui4cyou" -odooUid=I"1" +odooUid=I"2" diff --git a/openems-backend/config.d/Timedata/Dummy.config b/openems-backend/config.d/Timedata/Dummy.config deleted file mode 100644 index be5a68a..0000000 --- a/openems-backend/config.d/Timedata/Dummy.config +++ /dev/null @@ -1,2 +0,0 @@ -:org.apache.felix.configadmin.revision:=L"1" -service.pid="Timedata.Dummy" diff --git a/openems-backend/config.d/Timedata/InfluxDB/timedata0.config b/openems-backend/config.d/Timedata/InfluxDB/timedata0.config new file mode 100644 index 0000000..a5c763e --- /dev/null +++ b/openems-backend/config.d/Timedata/InfluxDB/timedata0.config @@ -0,0 +1,13 @@ +:org.apache.felix.configadmin.revision:=L"1" +id="timedata0" +queryLanguage="INFLUX_QL" +url="http://influxdb:8086" +org="-" +apiKey="root:root" +bucket="openemsdb/autogen" +measurement="data" +isReadOnly=B"false" +poolSize=I"10" +maxQueueSize=I"5000" +service.factoryPid="Timedata.InfluxDB" +service.pid="Timedata.InfluxDB.timedata0" diff --git a/openems-edge/config.d/Controller/Api/Backend/28628d01-a978-4328-9a8f-b9c7751ed2d2.config b/openems-edge/config.d/Controller/Api/Backend/28628d01-a978-4328-9a8f-b9c7751ed2d2.config index 1fa3282..e855d76 100644 --- a/openems-edge/config.d/Controller/Api/Backend/28628d01-a978-4328-9a8f-b9c7751ed2d2.config +++ b/openems-edge/config.d/Controller/Api/Backend/28628d01-a978-4328-9a8f-b9c7751ed2d2.config @@ -1,7 +1,7 @@ :org.apache.felix.configadmin.revision:=L"1" alias="backend" apiTimeout=I"60" -apikey="DEMO_API_KEY" +apikey="E3SZ5xdJ2FJ0jPMtajdm" debug=B"false" enabled=B"true" id="ctrlBackend0" diff --git a/openems-edge/config.d/Controller/Api/ModbusTcp/ReadOnly/17b969c5-fd9f-4cc3-9fba-fe7f41ff7146.config b/openems-edge/config.d/Controller/Api/ModbusTcp/ReadOnly/17b969c5-fd9f-4cc3-9fba-fe7f41ff7146.config index 6c21fe3..e386f17 100644 --- a/openems-edge/config.d/Controller/Api/ModbusTcp/ReadOnly/17b969c5-fd9f-4cc3-9fba-fe7f41ff7146.config +++ b/openems-edge/config.d/Controller/Api/ModbusTcp/ReadOnly/17b969c5-fd9f-4cc3-9fba-fe7f41ff7146.config @@ -1,14 +1,14 @@ -:org.apache.felix.configadmin.revision:=L"3" -Component.target="(&(enabled\=true)(!(service.pid\=Controller.Api.ModbusTcp.ReadOnly.17b969c5-fd9f-4cc3-9fba-fe7f41ff7146))(|(id\=_sum)))" -_lastChangeAt="2020-09-15T19:21:03" -_lastChangeBy="UNDEFINED" -alias="" -component.ids=[ \ - "_sum", \ - ] -enabled=B"false" -id="ctrlApiModbusTcp0" -maxConcurrentConnections=I"5" -port=I"502" -service.factoryPid="Controller.Api.ModbusTcp.ReadOnly" -service.pid="Controller.Api.ModbusTcp.ReadOnly.17b969c5-fd9f-4cc3-9fba-fe7f41ff7146" +:org.apache.felix.configadmin.revision:=L"4" +Component.target="(&(!(service.pid\=Controller.Api.ModbusTcp.ReadOnly.17b969c5-fd9f-4cc3-9fba-fe7f41ff7146))(|(id\=_sum)))" +_lastChangeAt="2020-09-15T19:21:03" +_lastChangeBy="UNDEFINED" +alias="" +component.ids=[ \ + "_sum", \ + ] +enabled=B"false" +id="ctrlApiModbusTcp0" +maxConcurrentConnections=I"5" +port=I"502" +service.factoryPid="Controller.Api.ModbusTcp.ReadOnly" +service.pid="Controller.Api.ModbusTcp.ReadOnly.17b969c5-fd9f-4cc3-9fba-fe7f41ff7146" diff --git a/openems-edge/config.d/Controller/IO/HeatingElement/b29d7d91-2b18-4db2-90ba-fd78224d6b0e.config b/openems-edge/config.d/Controller/IO/HeatingElement/b29d7d91-2b18-4db2-90ba-fd78224d6b0e.config index e2ec146..af1c1d0 100644 --- a/openems-edge/config.d/Controller/IO/HeatingElement/b29d7d91-2b18-4db2-90ba-fd78224d6b0e.config +++ b/openems-edge/config.d/Controller/IO/HeatingElement/b29d7d91-2b18-4db2-90ba-fd78224d6b0e.config @@ -1,18 +1,19 @@ -:org.apache.felix.configadmin.revision:=L"5" -_lastChangeAt="2020-09-15T19:38:09" -_lastChangeBy="guest:\ Guest" -alias="Control\ Heating\ Element" -defaultLevel="LEVEL_1" -enabled=B"true" -endTime="17:00" -id="ctrlIoHeatingElement0" -minTime=I"1" -minimumSwitchingTime=I"60" -mode="AUTOMATIC" -outputChannelPhaseL1="io0/InputOutput3" -outputChannelPhaseL2="io0/InputOutput4" -outputChannelPhaseL3="io0/InputOutput5" -powerPerPhase=I"2000" -service.factoryPid="Controller.IO.HeatingElement" -service.pid="Controller.IO.HeatingElement.b29d7d91-2b18-4db2-90ba-fd78224d6b0e" -workMode="TIME" +:org.apache.felix.configadmin.revision:=L"6" +_lastChangeAt="2020-09-15T19:38:09" +_lastChangeBy="guest:\ Guest" +alias="Control\ Heating\ Element" +defaultLevel="LEVEL_1" +enabled=B"true" +endTime="17:00" +id="ctrlIoHeatingElement0" +meter.target="(false\=true)" +minTime=I"1" +minimumSwitchingTime=I"60" +mode="AUTOMATIC" +outputChannelPhaseL1="io0/InputOutput3" +outputChannelPhaseL2="io0/InputOutput4" +outputChannelPhaseL3="io0/InputOutput5" +powerPerPhase=I"2000" +service.factoryPid="Controller.IO.HeatingElement" +service.pid="Controller.IO.HeatingElement.b29d7d91-2b18-4db2-90ba-fd78224d6b0e" +workMode="TIME" diff --git a/openems-edge/config.d/Controller/Symmetric/Balancing/6e15c5d6-3005-49a4-9a96-68fa5f39740f.config b/openems-edge/config.d/Controller/Symmetric/Balancing/6e15c5d6-3005-49a4-9a96-68fa5f39740f.config index 7f47aea..35680c3 100644 --- a/openems-edge/config.d/Controller/Symmetric/Balancing/6e15c5d6-3005-49a4-9a96-68fa5f39740f.config +++ b/openems-edge/config.d/Controller/Symmetric/Balancing/6e15c5d6-3005-49a4-9a96-68fa5f39740f.config @@ -1,9 +1,11 @@ -:org.apache.felix.configadmin.revision:=L"1" -alias="Self-consumption\ optimization" -enabled=B"true" -ess.id="ess0" -id="ctrlBalancing0" -meter.id="meter0" -service.factoryPid="Controller.Symmetric.Balancing" -service.pid="Controller.Symmetric.Balancing.6e15c5d6-3005-49a4-9a96-68fa5f39740f" -targetGridSetpoint=I"0" +:org.apache.felix.configadmin.revision:=L"3" +alias="Self-consumption\ optimization" +enabled=B"true" +ess.id="ess0" +ess.target="(&(enabled\=true)(!(service.pid\=Controller.Symmetric.Balancing.6e15c5d6-3005-49a4-9a96-68fa5f39740f))(|(id\=ess0)))" +id="ctrlBalancing0" +meter.id="meter0" +meter.target="(&(enabled\=true)(!(service.pid\=Controller.Symmetric.Balancing.6e15c5d6-3005-49a4-9a96-68fa5f39740f))(|(id\=meter0)))" +service.factoryPid="Controller.Symmetric.Balancing" +service.pid="Controller.Symmetric.Balancing.6e15c5d6-3005-49a4-9a96-68fa5f39740f" +targetGridSetpoint=I"0" diff --git a/openems-edge/config.d/Core/AppManager.config b/openems-edge/config.d/Core/AppManager.config new file mode 100644 index 0000000..1151d75 --- /dev/null +++ b/openems-edge/config.d/Core/AppManager.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.AppManager" +id="_appManager" +service.bundleLocation="?" +service.pid="Core.AppManager" diff --git a/openems-edge/config.d/Core/ComponentManager.config b/openems-edge/config.d/Core/ComponentManager.config new file mode 100644 index 0000000..09a3b34 --- /dev/null +++ b/openems-edge/config.d/Core/ComponentManager.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.ComponentManager" +id="_componentManager" +service.bundleLocation="?" +service.pid="Core.ComponentManager" diff --git a/openems-edge/config.d/Core/Cycle.config b/openems-edge/config.d/Core/Cycle.config new file mode 100644 index 0000000..7e88622 --- /dev/null +++ b/openems-edge/config.d/Core/Cycle.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.Cycle" +id="_cycle" +service.bundleLocation="?" +service.pid="Core.Cycle" diff --git a/openems-edge/config.d/Core/Energy.config b/openems-edge/config.d/Core/Energy.config new file mode 100644 index 0000000..8aa0587 --- /dev/null +++ b/openems-edge/config.d/Core/Energy.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.Energy" +id="_energy" +service.bundleLocation="?" +service.pid="Core.Energy" diff --git a/openems-edge/config.d/Core/Host.config b/openems-edge/config.d/Core/Host.config new file mode 100644 index 0000000..2e94229 --- /dev/null +++ b/openems-edge/config.d/Core/Host.config @@ -0,0 +1,9 @@ +:org.apache.felix.configadmin.revision:=L"4" +_lastChangeAt="2026-03-07T22:52:32" +_lastChangeBy="Internal\ NetworkConfigurationWorker" +alias="Core.Host" +id="_host" +networkConfiguration="{\n\ \ \"interfaces\":\ {}\n}" +service.bundleLocation="?" +service.pid="Core.Host" +usbConfiguration="" diff --git a/openems-edge/config.d/Core/Meta.config b/openems-edge/config.d/Core/Meta.config new file mode 100644 index 0000000..cff329b --- /dev/null +++ b/openems-edge/config.d/Core/Meta.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.Meta" +id="_meta" +service.bundleLocation="?" +service.pid="Core.Meta" diff --git a/openems-edge/config.d/Core/PredictorManager.config b/openems-edge/config.d/Core/PredictorManager.config new file mode 100644 index 0000000..c2e60e9 --- /dev/null +++ b/openems-edge/config.d/Core/PredictorManager.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.PredictorManager" +id="_predictorManager" +service.bundleLocation="?" +service.pid="Core.PredictorManager" diff --git a/openems-edge/config.d/Core/SerialNumber.config b/openems-edge/config.d/Core/SerialNumber.config new file mode 100644 index 0000000..1947607 --- /dev/null +++ b/openems-edge/config.d/Core/SerialNumber.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.SerialNumber" +id="_serialNumber" +service.bundleLocation="?" +service.pid="Core.SerialNumber" diff --git a/openems-edge/config.d/Core/Sum.config b/openems-edge/config.d/Core/Sum.config new file mode 100644 index 0000000..017e98e --- /dev/null +++ b/openems-edge/config.d/Core/Sum.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Core.Sum" +id="_sum" +service.bundleLocation="?" +service.pid="Core.Sum" diff --git a/openems-edge/config.d/Ess/Power.config b/openems-edge/config.d/Ess/Power.config new file mode 100644 index 0000000..3c73ac5 --- /dev/null +++ b/openems-edge/config.d/Ess/Power.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Ess.Power" +id="_power" +service.bundleLocation="?" +service.pid="Ess.Power" diff --git a/openems-edge/config.d/Evcs/SlowPowerIncreaseFilter.config b/openems-edge/config.d/Evcs/SlowPowerIncreaseFilter.config new file mode 100644 index 0000000..3d89419 --- /dev/null +++ b/openems-edge/config.d/Evcs/SlowPowerIncreaseFilter.config @@ -0,0 +1,5 @@ +:org.apache.felix.configadmin.revision:=L"1" +alias="Evcs.SlowPowerIncreaseFilter" +id="_evcsSlowPowerIncreaseFilter" +service.bundleLocation="?" +service.pid="Evcs.SlowPowerIncreaseFilter" diff --git a/openems-edge/config.d/org_apache_felix_cm_impl_DynamicBindings.config b/openems-edge/config.d/org_apache_felix_cm_impl_DynamicBindings.config index 8977e09..e5a9d0b 100644 --- a/openems-edge/config.d/org_apache_felix_cm_impl_DynamicBindings.config +++ b/openems-edge/config.d/org_apache_felix_cm_impl_DynamicBindings.config @@ -1 +1 @@ -org.ops4j.pax.logging="jar/pax-logging-log4j1-2.0.5.jar" +org.ops4j.pax.logging="jar/pax-logging-log4j2-2.3.1.jar" diff --git a/setup.sh b/setup.sh index b074041..a917e62 100755 --- a/setup.sh +++ b/setup.sh @@ -4,8 +4,10 @@ # Idempotent: skips steps that are already done. # # Usage: -# ./setup.sh # full setup (build + init + verify) -# ./setup.sh --skip-build # skip docker compose build (images already exist) +# ./setup.sh # full setup, 1 edge (default) +# ./setup.sh --edges 3 # full setup with 3 simulated edges +# ./setup.sh --skip-build # skip docker compose build (images already exist) +# ./setup.sh --edges 3 --skip-build set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -21,17 +23,184 @@ warn() { echo -e "${YELLOW}[setup]${NC} $*"; } err() { echo -e "${RED}[setup]${NC} $*" >&2; } SKIP_BUILD=false -for arg in "$@"; do - case "$arg" in - --skip-build) SKIP_BUILD=true ;; - *) err "Unknown argument: $arg"; exit 1 ;; +EDGE_COUNT=1 +while [ $# -gt 0 ]; do + case "$1" in + --skip-build) SKIP_BUILD=true; shift ;; + --edges) + if [ -z "${2:-}" ]; then + err "--edges requires a numeric argument" + exit 1 + fi + EDGE_COUNT="$2" + shift 2 + ;; + *) err "Unknown argument: $1"; exit 1 ;; esac done -# Expected edge apikey — must match openems-edge/config.d/Controller/Api/Backend/*.config -EDGE_APIKEY="E3SZ5xdJ2FJ0jPMtajdm" +# Validate --edges value +if ! [[ "$EDGE_COUNT" =~ ^[0-9]+$ ]] || [ "$EDGE_COUNT" -lt 1 ]; then + err "--edges must be a positive integer (got: $EDGE_COUNT)" + exit 1 +fi +if [ "$EDGE_COUNT" -gt 9 ]; then + err "--edges supports up to 9 edges (port scheme 8{i}80/8{i}85 collides at 10)" + exit 1 +fi + # Odoo password — must match openems-backend/config.d/Metadata/Odoo.config (odooPassword) ODOO_PASSWORD="Icui4cyou" +# Template directory for edge configs +EDGE_TEMPLATE_DIR="openems-edge/config.d" + +# ── Helper: generate random 20-char alphanumeric API key ────────────── +generate_apikey() { + # Matches Odoo model format: 20 chars from [a-zA-Z0-9] + # Disable pipefail locally: head -c 20 closes the pipe early, causing tr to + # receive SIGPIPE (exit 141) which pipefail would treat as a failure. + (set +o pipefail; LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1; then + # GNU sed + sed -i "$@" + else + # macOS/BSD sed + sed -i '' "$@" + fi +} + +# ── Helper: generate a new UUID ─────────────────────────────────────── +generate_uuid() { + if command -v uuidgen >/dev/null 2>&1; then + uuidgen | tr '[:upper:]' '[:lower:]' + else + # Fallback using /proc/sys/kernel/random/uuid (Linux) + cat /proc/sys/kernel/random/uuid 2>/dev/null || \ + python3 -c "import uuid; print(uuid.uuid4())" + fi +} + +# ── Helper: generate per-edge config directory from template ────────── +# Copies the template, regenerates UUIDs for factory configs, updates +# service.pid values and LDAP target filters, and writes the apikey. +# +# Args: $1 = edge index (0, 1, ...), $2 = apikey for this edge +generate_edge_config() { + local edge_idx="$1" + local apikey="$2" + local dest_dir="openems-edge/config-edge${edge_idx}" + + # Clean and copy template + rm -rf "$dest_dir" + cp -R "$EDGE_TEMPLATE_DIR" "$dest_dir" + + # Process each .config file + while IFS= read -r -d '' config_file; do + local rel_path="${config_file#${dest_dir}/}" + local filename + filename=$(basename "$config_file" .config) + local dir_path + dir_path=$(dirname "$config_file") + + # Determine if this is a UUID-named factory config + # UUID pattern: 8-4-4-4-12 hex chars + if [[ "$filename" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then + local old_uuid="$filename" + local new_uuid + new_uuid=$(generate_uuid) + + # Read the actual service.pid from the file to get the correct prefix + # (directory casing may differ from PID casing, e.g. Controller/IO vs Controller.Io) + local old_pid + old_pid=$(grep '^service\.pid=' "$config_file" | sed 's/service\.pid="//' | sed 's/"$//') + local pid_prefix="${old_pid%.${old_uuid}}" + local new_pid="${pid_prefix}.${new_uuid}" + + # Update service.pid in the file + sedi "s|service\\.pid=\"${old_pid}\"|service.pid=\"${new_pid}\"|g" "$config_file" + + # Update LDAP target filters that reference the old PID + # These appear in Component.target and datasource.target properties + # The PID is embedded with escaped equals: service.pid\=Old.Pid.uuid + sedi "s|service\\.pid\\\\=${old_pid}|service.pid\\\\=${new_pid}|g" "$config_file" + + # Rename the file to match the new UUID + mv "$config_file" "${dir_path}/${new_uuid}.config" + + elif [ "$filename" = "rrd4j0" ]; then + # Timedata/Rrd4j/rrd4j0.config — component-id filename, copy as-is + : + else + # Non-factory configs (logging, Felix bindings) — copy as-is + : + fi + done < <(find "$dest_dir" -name "*.config" -type f -print0) + + # Write the apikey into the Backend config + local backend_config + backend_config=$(find "$dest_dir/Controller/Api/Backend" -name "*.config" -type f | head -1) + if [ -n "$backend_config" ]; then + sedi "s|apikey=\"[^\"]*\"|apikey=\"${apikey}\"|" "$backend_config" + else + err "Backend config not found in $dest_dir" + exit 1 + fi + + log " Edge ${edge_idx}: config generated at ${dest_dir}" +} + +# ── Helper: generate docker-compose.override.yml ────────────────────── +generate_compose_override() { + local override_file="docker-compose.override.yml" + log "Generating ${override_file} for ${EDGE_COUNT} edge(s)..." + + cat > "$override_file" <<'HEADER' +# Generated by setup.sh — do not edit manually. +# This file is auto-merged by docker compose with docker-compose.yml. +services: + # Disable the default single-edge service from docker-compose.yml + openems-edge: + profiles: ["disabled"] +HEADER + + for i in $(seq 0 $((EDGE_COUNT - 1))); do + local felix_port="8${i}80" + local ws_port="8${i}85" + + cat >> "$override_file" </dev/null || echo "") if [ "$DB_EXISTS" = "1" ]; then @@ -80,8 +249,12 @@ else # odooPassword for XML-RPC calls (used for UI user authentication). log "Setting Odoo passwords to match backend config..." docker compose up -d odoo16 - sleep 5 - docker compose exec -T odoo16 python3 -c " + + # Wait for Odoo to be ready for XML-RPC calls (can take 15-30s on first boot) + ODOO_READY=false + for odoo_attempt in $(seq 1 12); do + sleep 5 + if docker compose exec -T odoo16 python3 -c " import xmlrpc.client url = 'http://localhost:8069' db = 'openems' @@ -90,47 +263,87 @@ models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object') models.execute_kw(db, uid, 'admin', 'res.users', 'write', [[uid], {'password': '$ODOO_PASSWORD'}]) models.execute_kw(db, uid, '$ODOO_PASSWORD', 'res.users', 'write', [[1], {'password': '$ODOO_PASSWORD'}]) print('Odoo passwords updated') -" +" 2>/dev/null; then + ODOO_READY=true + break + fi + log " Waiting for Odoo... (${odoo_attempt}/12)" + done + + if [ "$ODOO_READY" = false ]; then + warn "Could not set Odoo password via XML-RPC after 60 seconds." + warn "You may need to set it manually: admin / admin → admin / $ODOO_PASSWORD" + fi + docker compose stop odoo16 fi -# ── Step 4: Verify edge device registration ─────────────────────────── -EDGE_KEY=$(docker compose exec -T db psql -U odoo -d openems -tAc \ - "SELECT apikey FROM openems_device WHERE name='edge0'" 2>/dev/null | tr -d '[:space:]' || echo "") - -if [ -z "$EDGE_KEY" ]; then - warn "Edge device 'edge0' not found in database." - warn "Demo data may not have loaded. Creating edge device..." - docker compose exec -T db psql -U odoo -d openems -c \ - "INSERT INTO openems_device (name, apikey, comment, create_uid, create_date, write_uid, write_date) - VALUES ('edge0', '$EDGE_APIKEY', 'OpenEMS Edge #0', 1, NOW(), 1, NOW());" - log "Edge device created." -elif [ "$EDGE_KEY" != "$EDGE_APIKEY" ]; then - warn "Edge apikey mismatch: DB='$EDGE_KEY', expected='$EDGE_APIKEY'" - warn "Updating database to match edge config..." - docker compose exec -T db psql -U odoo -d openems -c \ - "UPDATE openems_device SET apikey='$EDGE_APIKEY' WHERE name='edge0';" - log "Edge apikey updated." -else - log "Edge device 'edge0' registered with correct apikey." -fi +# ── Step 4: Register edge devices in database ───────────────────────── +for i in $(seq 0 $((EDGE_COUNT - 1))); do + EDGE_NAME="edge${i}" + EDGE_APIKEY="${EDGE_APIKEYS[$i]}" -# ── Step 5: Validate Edge config ────────────────────────────────────── -if ! ls openems-edge/config.d/Timedata/Rrd4j/*.config 1>/dev/null 2>&1; then - err "Missing openems-edge/config.d/Timedata/Rrd4j/*.config — Edge needs RRD4j for energy channel calculation" - exit 1 -fi -log "RRD4j Timedata config verified." + EDGE_KEY=$(docker compose exec -T db psql -U odoo -d openems -tAc \ + "SELECT apikey FROM openems_device WHERE name='${EDGE_NAME}'" 2>/dev/null | tr -d '[:space:]' || echo "") + + if [ -z "$EDGE_KEY" ]; then + warn "Edge device '${EDGE_NAME}' not found in database. Creating..." + docker compose exec -T db psql -U odoo -d openems -c \ + "INSERT INTO openems_device (name, name_number, apikey, comment, active, create_uid, create_date, write_uid, write_date) + VALUES ('${EDGE_NAME}', ${i}, '${EDGE_APIKEY}', 'OpenEMS Edge #${i}', true, 1, NOW(), 1, NOW());" + log " ${EDGE_NAME} created with apikey ${EDGE_APIKEY}" + elif [ "$EDGE_KEY" != "$EDGE_APIKEY" ]; then + warn "Edge '${EDGE_NAME}' apikey mismatch: DB='${EDGE_KEY}', config='${EDGE_APIKEY}'" + warn "Updating database to match generated config..." + docker compose exec -T db psql -U odoo -d openems -c \ + "UPDATE openems_device SET apikey='${EDGE_APIKEY}' WHERE name='${EDGE_NAME}';" + log " ${EDGE_NAME} apikey updated." + else + log " ${EDGE_NAME} registered with correct apikey." + fi + + # Assign admin user (uid 2) to this edge so it appears in the UI. + # The openems_device_user_role table links users to edges; without an entry + # the edge is connected but invisible to the user in the OpenEMS UI. + DEVICE_ID=$(docker compose exec -T db psql -U odoo -d openems -tAc \ + "SELECT id FROM openems_device WHERE name='${EDGE_NAME}'" 2>/dev/null | tr -d '[:space:]') + if [ -n "$DEVICE_ID" ]; then + ROLE_EXISTS=$(docker compose exec -T db psql -U odoo -d openems -tAc \ + "SELECT 1 FROM openems_device_user_role WHERE device_id=${DEVICE_ID} AND user_id=2" 2>/dev/null | tr -d '[:space:]') + if [ "$ROLE_EXISTS" != "1" ]; then + docker compose exec -T db psql -U odoo -d openems -c \ + "INSERT INTO openems_device_user_role (device_id, user_id, role, create_uid, write_uid, create_date, write_date) + VALUES (${DEVICE_ID}, 2, 'admin', 1, 1, NOW(), NOW());" >/dev/null 2>&1 + log " ${EDGE_NAME} assigned to admin user." + fi + fi +done + +# ── Step 5: Validate Edge configs ───────────────────────────────────── +for i in $(seq 0 $((EDGE_COUNT - 1))); do + config_dir="openems-edge/config-edge${i}" + if ! ls "${config_dir}/Timedata/Rrd4j/"*.config 1>/dev/null 2>&1; then + err "Missing ${config_dir}/Timedata/Rrd4j/*.config — Edge needs RRD4j for energy channel calculation" + exit 1 + fi + if ! ls "${config_dir}/Controller/Api/Backend/"*.config 1>/dev/null 2>&1; then + err "Missing ${config_dir}/Controller/Api/Backend/*.config — Edge needs Backend controller" + exit 1 + fi +done +log "Edge configs validated for ${EDGE_COUNT} edge(s)." # ── Step 6: Start the full stack ────────────────────────────────────── log "Starting all services..." docker compose up -d -# The edge doesn't auto-reconnect quickly if it started before the backend -# was ready. Restart it to ensure a clean connection. -log "Restarting edge to ensure backend connection..." +# The edges don't auto-reconnect quickly if they started before the backend +# was ready. Restart them to ensure clean connections. +log "Restarting edge(s) to ensure backend connection..." sleep 5 -docker compose restart openems-edge +for i in $(seq 0 $((EDGE_COUNT - 1))); do + docker compose restart "openems-edge-${i}" +done # ── Step 7: Verify the stack (retry loop) ───────────────────────────── log "Waiting for services to start..." @@ -150,10 +363,13 @@ for attempt in $(seq 1 12); do check_logs openems-backend "Caching Edges.*finished" || PASS=false check_logs openems-backend "InfluxDB" || PASS=false - check_logs openems-edge "Scheduler" || PASS=false - check_logs openems-edge "Rrd4j" || PASS=false check_logs openems-backend "Edge.Websocket" || PASS=false + for i in $(seq 0 $((EDGE_COUNT - 1))); do + check_logs "openems-edge-${i}" "Scheduler" || PASS=false + check_logs "openems-edge-${i}" "Rrd4j" || PASS=false + done + if [ "$PASS" = true ]; then CHECKS_PASSED=true break @@ -166,29 +382,44 @@ log "=== Verification ===" if [ "$CHECKS_PASSED" = true ]; then log " Backend -> Postgres: OK" log " Backend -> InfluxDB: OK" - log " Edge scheduler: OK" - log " Edge RRD4j: OK" - log " Edge -> Backend: OK" + log " Backend websocket: OK" + for i in $(seq 0 $((EDGE_COUNT - 1))); do + log " Edge ${i} scheduler: OK" + log " Edge ${i} RRD4j: OK" + done log "" log "All checks passed!" else check_logs openems-backend "Caching Edges.*finished" && log " Backend -> Postgres: OK" || warn " Backend -> Postgres: FAILED" check_logs openems-backend "InfluxDB" && log " Backend -> InfluxDB: OK" || warn " Backend -> InfluxDB: FAILED" - check_logs openems-edge "Scheduler" && log " Edge scheduler: OK" || warn " Edge scheduler: FAILED" - check_logs openems-edge "Rrd4j" && log " Edge RRD4j: OK" || warn " Edge RRD4j: FAILED" - check_logs openems-backend "Edge.Websocket" && log " Edge -> Backend: OK" || warn " Edge -> Backend: FAILED" + check_logs openems-backend "Edge.Websocket" && log " Backend websocket: OK" || warn " Backend websocket: FAILED" + for i in $(seq 0 $((EDGE_COUNT - 1))); do + check_logs "openems-edge-${i}" "Scheduler" && log " Edge ${i} scheduler: OK" || warn " Edge ${i} scheduler: FAILED" + check_logs "openems-edge-${i}" "Rrd4j" && log " Edge ${i} RRD4j: OK" || warn " Edge ${i} RRD4j: FAILED" + done log "" warn "Some checks failed after 2 minutes." - warn "Check logs: docker compose logs --tail=50 openems-backend openems-edge" + warn "Check logs: docker compose logs --tail=50 openems-backend" + for i in $(seq 0 $((EDGE_COUNT - 1))); do + warn " docker compose logs --tail=50 openems-edge-${i}" + done fi log "" -log "Stack is ready:" +log "Stack is ready (${EDGE_COUNT} edge(s)):" log " OpenEMS UI: http://localhost:4200" log " Odoo: http://localhost:10016" -log " Felix Console: http://localhost:8080" log " InfluxDB: http://localhost:8086" +for i in $(seq 0 $((EDGE_COUNT - 1))); do + log " Edge ${i} Felix: http://localhost:8${i}80" + log " Edge ${i} WS: ws://localhost:8${i}85" +done log "" log "Default credentials:" log " OpenEMS UI: admin / Icui4cyou" log " Odoo: admin / Icui4cyou (master pw: openemspassword)" +log "" +log "Edge API keys:" +for i in $(seq 0 $((EDGE_COUNT - 1))); do + log " edge${i}: ${EDGE_APIKEYS[$i]}" +done