From 2ba485f92bcd24499f84f3cdc59c40f9e5bc539b Mon Sep 17 00:00:00 2001 From: Long Zheng Date: Fri, 20 Dec 2024 16:36:08 +1100 Subject: [PATCH 1/2] Make InfluxDB optional --- .env.example | 12 +-- docker-compose.yml | 41 ++++----- src/helpers/env.ts | 12 +-- src/helpers/influxdb.ts | 182 ++++++++++++++++++++++++++++------------ 4 files changed, 160 insertions(+), 87 deletions(-) diff --git a/.env.example b/.env.example index f712d43..8752d6a 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,9 @@ CONFIG_DIR=./config SEP2_CERT_FILE=cert.pem SEP2_KEY_FILE=key.pem SEP2_PEN=62223 -INFLUXDB_USERNAME=admin -INFLUXDB_PASSWORD=password -INFLUXDB_ADMIN_TOKEN=super-secret-auth-token -INFLUXDB_ORG=open-dynamic-export -INFLUXDB_BUCKET=data -INFLUXDB_PORT=8086 \ No newline at end of file +# INFLUXDB_USERNAME=admin +# INFLUXDB_PASSWORD=password +# INFLUXDB_ADMIN_TOKEN=super-secret-auth-token +# INFLUXDB_ORG=open-dynamic-export +# INFLUXDB_BUCKET=data +# INFLUXDB_PORT=8086 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0d00d1c..8d158f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,26 +13,27 @@ services: - ./config/:/app/config/ - ./logs/:/app/logs/ restart: 'unless-stopped' - depends_on: - - influxdb - influxdb: - image: influxdb:2 - env_file: - - .env - ports: - - ${INFLUXDB_PORT}:${INFLUXDB_PORT} - environment: - DOCKER_INFLUXDB_INIT_MODE: setup - INFLUXD_HTTP_BIND_ADDRESS: :${INFLUXDB_PORT} - DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUXDB_USERNAME} - DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUXDB_PASSWORD} - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUXDB_ADMIN_TOKEN} - DOCKER_INFLUXDB_INIT_ORG: ${INFLUXDB_ORG} - DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUXDB_BUCKET} - volumes: - - influxdb2-data:/var/lib/influxdb2 - - influxdb2-config:/etc/influxdb2 - restart: 'unless-stopped' + # depends_on: + # - influxdb + + # influxdb: + # image: influxdb:2 + # env_file: + # - .env + # ports: + # - ${INFLUXDB_PORT}:${INFLUXDB_PORT} + # environment: + # DOCKER_INFLUXDB_INIT_MODE: setup + # INFLUXD_HTTP_BIND_ADDRESS: :${INFLUXDB_PORT} + # DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUXDB_USERNAME} + # DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUXDB_PASSWORD} + # DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUXDB_ADMIN_TOKEN} + # DOCKER_INFLUXDB_INIT_ORG: ${INFLUXDB_ORG} + # DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUXDB_BUCKET} + # volumes: + # - influxdb2-data:/var/lib/influxdb2 + # - influxdb2-config:/etc/influxdb2 + # restart: 'unless-stopped' volumes: influxdb2-data: influxdb2-config: \ No newline at end of file diff --git a/src/helpers/env.ts b/src/helpers/env.ts index 372d9b2..f80f813 100644 --- a/src/helpers/env.ts +++ b/src/helpers/env.ts @@ -10,12 +10,12 @@ const envSchema = z.object({ SEP2_CERT_FILE: z.string(), SEP2_KEY_FILE: z.string(), SEP2_PEN: z.string(), - INFLUXDB_USERNAME: z.string(), - INFLUXDB_PASSWORD: z.string(), - INFLUXDB_ADMIN_TOKEN: z.string(), - INFLUXDB_ORG: z.string(), - INFLUXDB_BUCKET: z.string(), - INFLUXDB_PORT: z.string().transform(safeParseIntString), + INFLUXDB_USERNAME: z.string().optional(), + INFLUXDB_PASSWORD: z.string().optional(), + INFLUXDB_ADMIN_TOKEN: z.string().optional(), + INFLUXDB_ORG: z.string().optional(), + INFLUXDB_BUCKET: z.string().optional(), + INFLUXDB_PORT: z.string().transform(safeParseIntString).optional(), }); const parsedEnv = envSchema.safeParse(process.env); diff --git a/src/helpers/influxdb.ts b/src/helpers/influxdb.ts index a2b3719..a22c0aa 100644 --- a/src/helpers/influxdb.ts +++ b/src/helpers/influxdb.ts @@ -12,26 +12,44 @@ import { } from '../coordinator/helpers/inverterController.js'; import { type FallbackControl } from '../sep2/helpers/fallbackControl.js'; import { objectEntriesWithType } from './object.js'; +import { env } from './env.js'; -const influxDB = new InfluxDB({ - url: `http://influxdb:${process.env['INFLUXDB_PORT'] ?? 8086}`, - token: process.env['INFLUXDB_ADMIN_TOKEN'], - writeOptions: { - flushInterval: 5_000, - }, -}); +const influxDB = (() => { + const org = env.INFLUXDB_ORG; + const bucket = env.INFLUXDB_BUCKET; + const port = env.INFLUXDB_PORT; + const token = env.INFLUXDB_ADMIN_TOKEN; -const queryApi = influxDB.getQueryApi(process.env['INFLUXDB_ORG']!); + if (!org || !bucket || !port || !token) { + return null; + } + + const db = new InfluxDB({ + url: `http://influxdb:${port}`, + token, + writeOptions: { + flushInterval: 5_000, + }, + }); + + const queryApi = db.getQueryApi(org); -const writeApi = influxDB.getWriteApi( - process.env['INFLUXDB_ORG']!, - process.env['INFLUXDB_BUCKET']!, -); + const writeApi = db.getWriteApi(org, bucket); + + return { + queryApi, + writeApi, + }; +})(); export function writeSiteSamplePoints(siteSample: SiteSample) { + if (!influxDB) { + return; + } + switch (siteSample.realPower.type) { case 'noPhase': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -41,7 +59,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { break; } case 'perPhaseNet': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -50,7 +68,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { ); if (siteSample.realPower.phaseA) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -60,7 +78,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.realPower.phaseB) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -70,7 +88,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.realPower.phaseC) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -83,7 +101,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { switch (siteSample.reactivePower.type) { case 'noPhase': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -93,7 +111,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { break; } case 'perPhaseNet': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -102,7 +120,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { ); if (siteSample.reactivePower.phaseA) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -115,7 +133,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.reactivePower.phaseB) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -128,7 +146,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.reactivePower.phaseC) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -143,7 +161,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.voltage.phaseA) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -153,7 +171,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.voltage.phaseB) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -163,7 +181,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.voltage.phaseC) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -173,7 +191,7 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } if (siteSample.frequency) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(siteSample.date) .tag('type', 'site') @@ -183,9 +201,13 @@ export function writeSiteSamplePoints(siteSample: SiteSample) { } export function writeDerSamplePoints(derSample: DerSample) { + if (!influxDB) { + return; + } + switch (derSample.realPower.type) { case 'noPhase': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -195,7 +217,7 @@ export function writeDerSamplePoints(derSample: DerSample) { break; } case 'perPhaseNet': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -204,7 +226,7 @@ export function writeDerSamplePoints(derSample: DerSample) { ); if (derSample.realPower.phaseA) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -214,7 +236,7 @@ export function writeDerSamplePoints(derSample: DerSample) { } if (derSample.realPower.phaseB) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -224,7 +246,7 @@ export function writeDerSamplePoints(derSample: DerSample) { } if (derSample.realPower.phaseC) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -237,7 +259,7 @@ export function writeDerSamplePoints(derSample: DerSample) { switch (derSample.reactivePower.type) { case 'noPhase': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -247,7 +269,7 @@ export function writeDerSamplePoints(derSample: DerSample) { break; } case 'perPhaseNet': { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -256,7 +278,7 @@ export function writeDerSamplePoints(derSample: DerSample) { ); if (derSample.reactivePower.phaseA) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -269,7 +291,7 @@ export function writeDerSamplePoints(derSample: DerSample) { } if (derSample.reactivePower.phaseB) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -282,7 +304,7 @@ export function writeDerSamplePoints(derSample: DerSample) { } if (derSample.reactivePower.phaseC) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -298,7 +320,7 @@ export function writeDerSamplePoints(derSample: DerSample) { if (derSample.voltage) { if (derSample.voltage.phaseA) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -308,7 +330,7 @@ export function writeDerSamplePoints(derSample: DerSample) { } if (derSample.voltage.phaseB) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -318,7 +340,7 @@ export function writeDerSamplePoints(derSample: DerSample) { } if (derSample.voltage.phaseC) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -329,7 +351,7 @@ export function writeDerSamplePoints(derSample: DerSample) { } if (derSample.frequency) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('sample') .timestamp(derSample.date) .tag('type', 'der') @@ -347,6 +369,10 @@ export function writeControlSchedulerPoints({ activeControlSchedule: RandomizedControlSchedule | null; fallbackControl: FallbackControl; }) { + if (!influxDB) { + return; + } + const activeControlPoint = (() => { if (!activeControlSchedule) { return null; @@ -530,7 +556,7 @@ export function writeControlSchedulerPoints({ return point; })(); - writeApi.writePoints( + influxDB.writeApi.writePoints( [activeControlPoint, fallbackControlPoint].filter( (point) => point !== null, ), @@ -556,7 +582,11 @@ export function writeInverterControllerPoints({ targetSolarWatts: number; targetSolarPowerRatio: number; }) { - writeApi.writePoint( + if (!influxDB) { + return; + } + + influxDB.writeApi.writePoint( new Point('inverterControl') .booleanField('disconnect', disconnect) .floatField('siteWatts', siteWatts) @@ -573,14 +603,24 @@ export function writeInverterControllerPoints({ } export function writeAmberPrice(number: number | undefined) { + if (!influxDB) { + return; + } + if (number === undefined) { return; } - writeApi.writePoint(new Point('amber').floatField('price', number)); + influxDB.writeApi.writePoint( + new Point('amber').floatField('price', number), + ); } export function writeControlLimit({ limit }: { limit: InverterControlLimit }) { + if (!influxDB) { + return; + } + const point = new Point('controlLimit').tag('name', limit.source); if (limit.opModConnect !== undefined) { @@ -599,7 +639,7 @@ export function writeControlLimit({ limit }: { limit: InverterControlLimit }) { point.floatField('opModGenLimW', limit.opModGenLimW); } - writeApi.writePoint(point); + influxDB.writeApi.writePoint(point); } export function writeActiveControlLimit({ @@ -607,8 +647,12 @@ export function writeActiveControlLimit({ }: { limit: ActiveInverterControlLimit; }) { + if (!influxDB) { + return; + } + if (limit.opModConnect !== undefined) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('activeControlLimit') .tag('name', limit.opModConnect.source) .booleanField('opModConnect', limit.opModConnect.value), @@ -616,7 +660,7 @@ export function writeActiveControlLimit({ } if (limit.opModEnergize !== undefined) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('activeControlLimit') .tag('name', limit.opModEnergize.source) .booleanField('opModEnergize', limit.opModEnergize.value), @@ -624,7 +668,7 @@ export function writeActiveControlLimit({ } if (limit.opModExpLimW !== undefined) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('activeControlLimit') .tag('name', limit.opModExpLimW.source) .floatField('opModExpLimW', limit.opModExpLimW.value), @@ -632,7 +676,7 @@ export function writeActiveControlLimit({ } if (limit.opModGenLimW !== undefined) { - writeApi.writePoint( + influxDB.writeApi.writePoint( new Point('activeControlLimit') .tag('name', limit.opModGenLimW.source) .floatField('opModGenLimW', limit.opModGenLimW.value), @@ -641,7 +685,11 @@ export function writeActiveControlLimit({ } export function queryRealPowerSite() { - return queryApi.collectRows<{ + if (!influxDB) { + throw new Error("InfluxDB isn't available"); + } + + return influxDB.queryApi.collectRows<{ phase: string; type: string; _time: string; @@ -658,7 +706,11 @@ from(bucket: "data") } export function queryExportLimit() { - return queryApi.collectRows<{ + if (!influxDB) { + throw new Error("InfluxDB isn't available"); + } + + return influxDB.queryApi.collectRows<{ name: string; _measurement: string; _time: string; @@ -676,7 +728,11 @@ from(bucket: "data") } export function queryGenerationLimit() { - return queryApi.collectRows<{ + if (!influxDB) { + throw new Error("InfluxDB isn't available"); + } + + return influxDB.queryApi.collectRows<{ name: string; _measurement: string; _time: string; @@ -694,7 +750,11 @@ from(bucket: "data") } export function queryConnection() { - return queryApi.collectRows<{ + if (!influxDB) { + throw new Error("InfluxDB isn't available"); + } + + return influxDB.queryApi.collectRows<{ name: string; _measurement: string; _time: string; @@ -712,7 +772,11 @@ from(bucket: "data") } export function queryEnergize() { - return queryApi.collectRows<{ + if (!influxDB) { + throw new Error("InfluxDB isn't available"); + } + + return influxDB.queryApi.collectRows<{ name: string; _measurement: string; _time: string; @@ -730,7 +794,11 @@ from(bucket: "data") } export function writeLoadWatts(loadWatts: number) { - writeApi.writePoint( + if (!influxDB) { + return; + } + + influxDB.writeApi.writePoint( new Point('sample') .timestamp(new Date()) .tag('type', 'load') @@ -748,6 +816,10 @@ export function writeLatency({ duration: number; tags?: Record; }) { + if (!influxDB) { + return; + } + const point = new Point('latency') .timestamp(new Date()) .floatField(field, duration); @@ -758,5 +830,5 @@ export function writeLatency({ } } - writeApi.writePoint(point); + influxDB.writeApi.writePoint(point); } From 3f7bda75f549a567e0db7fe4503819057a06e35d Mon Sep 17 00:00:00 2001 From: Long Zheng Date: Fri, 20 Dec 2024 16:43:10 +1100 Subject: [PATCH 2/2] Update docs --- docs/guide/index.md | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/guide/index.md b/docs/guide/index.md index 010c05d..6e45d7b 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -52,28 +52,41 @@ The system supports one site meter to measure the site's load and export metrics ## Install -### Docker compose - 1. Clone Git repo 2. Copy `.env.example` and rename it to `.env` and change the values to suit your environment ```yaml -TZ=Australia/Melbourne # Your system timezone +TZ=Australia/Melbourne # System timezone SERVER_PORT=3000 # API server port CONFIG_DIR=./config # Directory to store configuration files -SEP2_CERT_FILE=sapn_cert.pem # CSIP-AUS/SEP2 certificate file (in the config directory) -SEP2_KEY_FILE=sapn_key.pem # CSIP-AUS/SEP2 key file (in the config directory) -SEP2_PEN=62223 # CSIP-AUS/SEP2 Private Enterprise Number -INFLUXDB_USERNAME=admin # InfluxDB username -INFLUXDB_PASSWORD=password # InfluxDB password -INFLUXDB_ADMIN_TOKEN=super-secret-auth-token # InfluxDB admin token -INFLUXDB_ORG=open-dynamic-export # InfluxDB organisation -INFLUXDB_BUCKET=data # InfluxDB bucket -INFLUXDB_PORT=8086 # InfluxDB port -LOGLEVEL=debug # Log level (valid values: trace, debug) +SEP2_CERT_FILE=sapn_cert.pem # CSIP-AUS/SEP2 certificate file path (in the config directory) +SEP2_KEY_FILE=sapn_key.pem # CSIP-AUS/SEP2 key file path (in the config directory) +SEP2_PEN=62223 # CSIP-AUS/SEP2 Provider Private Enterprise Number (does not need to be changed) +# INFLUXDB_USERNAME=admin # Optional InfluxDB username, enable to log data to InfluxDB +# INFLUXDB_PASSWORD=password # Optional InfluxDB password, enable to log data to InfluxDB +# INFLUXDB_ADMIN_TOKEN=super-secret-auth-token # Optional InfluxDB admin token, enable to log data to InfluxDB +# INFLUXDB_ORG=open-dynamic-export # Optional InfluxDB organisation, enable to log data to InfluxDB +# INFLUXDB_BUCKET=data # Optional InfluxDB bucket, enable to log data to InfluxDB +# INFLUXDB_PORT=8086 # Optional InfluxDB port, enable to log data to InfluxDB +# LOGLEVEL=debug # Optional log level (valid values: trace, debug. default: debug) ``` 3. In the `/config` folder, make a copy of the `config.example.json` file and rename it to `config.json`. Update it with the relevant values, see the "Configuration" section for more details. -4. Run `docker compose up -d` (optionally run `docker compose up -d --build` to build the image from the source code) \ No newline at end of file +Use Node or Docker to run the project. + +### Node +You can run the Node project directly with the Node.js runtime. + +1. Install dependencies with `npm install` + +2. Build the project with `npm run build` + +3. Run the project with `npm start` + +### Docker compose + +1. Run `docker compose up -d` to use from the Docker Hub image (optionally run `docker compose up -d --build` to build the image from the source code) + +2. Optionally uncomment the `influxdb` service in the `docker-compose.yml` file to enable logging to InfluxDB \ No newline at end of file