Compare commits

...

5 Commits

Author SHA1 Message Date
198944649a feat: Add new 1x1200/35 product configuration to data files and datasheets, including scripts to manage its technical and ampacity values.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m37s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m7s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 44s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 1m18s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-04 17:52:21 +01:00
6aa741ab0a refactor: remove npm cache restoration steps and update Gotify failure notification conditions
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 8s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m43s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 22s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m21s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 44s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 6m6s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 2s
2026-02-02 18:43:21 +01:00
f69952a5da refactor: Enable pino-pretty transport exclusively for development environments.
All checks were successful
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m6s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 22s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 11m52s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 42s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Successful in 4m40s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-02 17:43:54 +01:00
81af9bf3dd feat: Add Gotify notification service and integrate it with error reporting and contact form actions, making error reporting methods asynchronous.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m3s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 20s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been cancelled
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Has been cancelled
Build & Deploy KLZ Cables / 🔔 Notifications (push) Has been cancelled
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Has been cancelled
2026-02-02 17:38:15 +01:00
f1b617e967 feat: add contact form submission status translations 2026-02-02 17:34:18 +01:00
24 changed files with 565 additions and 42 deletions

View File

@@ -168,15 +168,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: 📦 Restore npm cache
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
@@ -427,7 +418,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
@@ -533,7 +523,13 @@ jobs:
-F "priority=4" || true
- name: 🔔 Gotify - Failure
if: needs.deploy.result == 'failure' || needs.build.result == 'failure' || needs.qa.result == 'failure'
if: |
needs.prepare.result == 'failure' ||
needs.qa.result == 'failure' ||
needs.build-app.result == 'failure' ||
needs.build-gatekeeper.result == 'failure' ||
needs.deploy.result == 'failure' ||
needs.pagespeed.result == 'failure'
run: |
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=❌ Deployment FEHLGESCHLAGEN ${{ needs.prepare.outputs.target || 'unknown' }}" \

View File

@@ -69,9 +69,19 @@ export async function sendContactFormAction(formData: FormData) {
if (result.success) {
logger.info('Contact form email sent successfully', { messageId: result.messageId });
await services.notifications.notify({
title: `📩 ${subject}`,
message: `New message from ${name} (${email}):\n\n${message}`,
priority: 5,
});
} else {
logger.error('Failed to send contact form email', { error: result.error });
services.errors.captureException(result.error, { action: 'sendContactFormAction', email });
await services.notifications.notify({
title: '🚨 Contact Form Error',
message: `Failed to send email for ${name} (${email}). Error: ${JSON.stringify(result.error)}`,
priority: 8,
});
}
return result;

Binary file not shown.

View File

@@ -398,6 +398,24 @@ locale: de
"55",
"4195"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"0,95",
"48,5",
"0,0247",
"3,4",
"Auf Anfrage",
"Auf Anfrage",
"113",
"2,4",
"885",
"59",
"4800"
]
}
]
},
@@ -737,6 +755,24 @@ locale: de
"60",
"4634"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1,05",
"52,3",
"0,0247",
"5,5",
"Auf Anfrage",
"Auf Anfrage",
"113",
"2,4",
"990",
"66",
"5200"
]
}
]
},
@@ -1076,6 +1112,24 @@ locale: de
"65",
"5093"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1,15",
"57,5",
"0,0247",
"8,0",
"Auf Anfrage",
"Auf Anfrage",
"113",
"2,4",
"1065",
"71",
"5900"
]
}
]
}

View File

@@ -398,6 +398,24 @@ locale: en
"55",
"4195"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"0.95",
"48.5",
"0.0247",
"3.4",
"On Request",
"On Request",
"113",
"2.4",
"885",
"59",
"4800"
]
}
]
},
@@ -737,6 +755,24 @@ locale: en
"60",
"4634"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1.05",
"52.3",
"0.0247",
"5.5",
"On Request",
"On Request",
"113",
"2.4",
"990",
"66",
"5200"
]
}
]
},
@@ -1076,6 +1112,24 @@ locale: en
"65",
"5093"
]
},
{
"configuration": "1x1200/35",
"cells": [
"Al",
"RM",
"1.15",
"57.5",
"0.0247",
"8",
"On Request",
"On Request",
"113",
"2.4",
"1065",
"71",
"5900"
]
}
]
}

View File

@@ -69,6 +69,13 @@ function createConfig() {
internalUrl: env.INTERNAL_DIRECTUS_URL,
proxyPath: '/cms',
},
notifications: {
gotify: {
url: env.GOTIFY_URL,
token: env.GOTIFY_TOKEN,
enabled: Boolean(env.GOTIFY_URL && env.GOTIFY_TOKEN),
},
},
} as const;
}
@@ -127,6 +134,9 @@ export const config = {
get directus() {
return getConfig().directus;
},
get notifications() {
return getConfig().notifications;
},
};
/**
@@ -171,5 +181,12 @@ export function getMaskedConfig() {
password: mask(c.directus.password),
token: mask(c.directus.token),
},
notifications: {
gotify: {
url: c.notifications.gotify.url,
token: mask(c.notifications.gotify.token),
enabled: c.notifications.gotify.enabled,
},
},
};
}

View File

@@ -49,6 +49,9 @@ export const envSchema = z.object({
// Deploy Target
TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(),
// Gotify
GOTIFY_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()),
GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()),
});
export type Env = z.infer<typeof envSchema>;
@@ -78,5 +81,7 @@ export function getRawEnv() {
DIRECTUS_API_TOKEN: process.env.DIRECTUS_API_TOKEN,
INTERNAL_DIRECTUS_URL: process.env.INTERNAL_DIRECTUS_URL,
TARGET: process.env.TARGET,
GOTIFY_URL: process.env.GOTIFY_URL,
GOTIFY_TOKEN: process.env.GOTIFY_TOKEN,
};
}

View File

@@ -2,6 +2,7 @@ import type { AnalyticsService } from './analytics/analytics-service';
import type { CacheService } from './cache/cache-service';
import type { ErrorReportingService } from './errors/error-reporting-service';
import type { LoggerService } from './logging/logger-service';
import type { NotificationService } from './notifications/notification-service';
// Simple constructor-based DI container.
export class AppServices {
@@ -9,6 +10,7 @@ export class AppServices {
public readonly analytics: AnalyticsService,
public readonly errors: ErrorReportingService,
public readonly cache: CacheService,
public readonly logger: LoggerService
public readonly logger: LoggerService,
public readonly notifications: NotificationService,
) {}
}

View File

@@ -4,6 +4,10 @@ import { UmamiAnalyticsService } from './analytics/umami-analytics-service';
import { MemoryCacheService } from './cache/memory-cache-service';
import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporting-service';
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
import {
GotifyNotificationService,
NoopNotificationService,
} from './notifications/gotify-notification-service';
import { PinoLoggerService } from './logging/pino-logger-service';
import { config, getMaskedConfig } from '../config';
@@ -23,6 +27,7 @@ export function getServerAppServices(): AppServices {
umamiEnabled: config.analytics.umami.enabled,
sentryEnabled: config.errors.glitchtip.enabled,
mailEnabled: Boolean(config.mail.host && config.mail.user),
gotifyEnabled: config.notifications.gotify.enabled,
});
const analytics = config.analytics.umami.enabled
@@ -35,8 +40,22 @@ export function getServerAppServices(): AppServices {
logger.info('Noop analytics service initialized (analytics disabled)');
}
const notifications = config.notifications.gotify.enabled
? new GotifyNotificationService({
url: config.notifications.gotify.url!,
token: config.notifications.gotify.token!,
enabled: true,
})
: new NoopNotificationService();
if (config.notifications.gotify.enabled) {
logger.info('Gotify notification service initialized');
} else {
logger.info('Noop notification service initialized (notifications disabled)');
}
const errors = config.errors.glitchtip.enabled
? new GlitchtipErrorReportingService({ enabled: true })
? new GlitchtipErrorReportingService({ enabled: true }, notifications)
: new NoopErrorReportingService();
if (config.errors.glitchtip.enabled) {
@@ -55,7 +74,7 @@ export function getServerAppServices(): AppServices {
level: config.logging.level,
});
singleton = new AppServices(analytics, errors, cache, logger);
singleton = new AppServices(analytics, errors, cache, logger, notifications);
logger.info('All application services initialized successfully');

View File

@@ -5,6 +5,7 @@ import { GlitchtipErrorReportingService } from './errors/glitchtip-error-reporti
import { NoopErrorReportingService } from './errors/noop-error-reporting-service';
import { NoopLoggerService } from './logging/noop-logger-service';
import { PinoLoggerService } from './logging/pino-logger-service';
import { NoopNotificationService } from './notifications/gotify-notification-service';
import { config, getMaskedConfig } from '../config';
/**
@@ -71,9 +72,7 @@ export function getAppServices(): AppServices {
// Create logger first to log initialization
const logger =
typeof window === 'undefined'
? new PinoLoggerService('server')
: new NoopLoggerService();
typeof window === 'undefined' ? new PinoLoggerService('server') : new NoopLoggerService();
// Log initialization
if (typeof window === 'undefined') {
@@ -121,7 +120,9 @@ export function getAppServices(): AppServices {
: new NoopErrorReportingService();
if (sentryEnabled) {
logger.info(`GlitchTip error reporting service initialized (${typeof window === 'undefined' ? 'server' : 'client'})`);
logger.info(
`GlitchTip error reporting service initialized (${typeof window === 'undefined' ? 'server' : 'client'})`,
);
} else {
logger.info('Noop error reporting service initialized (error reporting disabled)');
}
@@ -138,9 +139,10 @@ export function getAppServices(): AppServices {
});
// Create and cache the singleton
singleton = new AppServices(analytics, errors, cache, logger);
const notifications = new NoopNotificationService();
singleton = new AppServices(analytics, errors, cache, logger, notifications);
logger.info('All application services initialized successfully');
return singleton;
}

View File

@@ -7,10 +7,15 @@ export type ErrorReportingUser = {
export type ErrorReportingLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'log';
export interface ErrorReportingService {
captureException(error: unknown, context?: Record<string, unknown>): string | undefined;
captureMessage(message: string, level?: ErrorReportingLevel): string | undefined;
captureException(
error: unknown,
context?: Record<string, unknown>,
): Promise<string | undefined> | string | undefined;
captureMessage(
message: string,
level?: ErrorReportingLevel,
): Promise<string | undefined> | string | undefined;
setUser(user: ErrorReportingUser | null): void;
setTag(key: string, value: string): void;
withScope<T>(fn: () => T, context?: Record<string, unknown>): T;
}

View File

@@ -4,6 +4,7 @@ import type {
ErrorReportingService,
ErrorReportingUser,
} from './error-reporting-service';
import type { NotificationService } from '../notifications/notification-service';
type SentryLike = typeof Sentry;
@@ -15,12 +16,29 @@ export type GlitchtipErrorReportingServiceOptions = {
export class GlitchtipErrorReportingService implements ErrorReportingService {
constructor(
private readonly options: GlitchtipErrorReportingServiceOptions,
private readonly sentry: SentryLike = Sentry
private readonly notifications?: NotificationService,
private readonly sentry: SentryLike = Sentry,
) {}
captureException(error: unknown, context?: Record<string, unknown>) {
async captureException(error: unknown, context?: Record<string, unknown>) {
if (!this.options.enabled) return undefined;
return this.sentry.captureException(error, context as any) as any;
const result = this.sentry.captureException(error, context as any) as any;
// Send to Gotify if it's considered critical or if we just want all exceptions there
// For now, let's send all exceptions to Gotify as requested "notify me via gotify about critical error messages"
// We'll treat all captureException calls as potentially critical or at least noteworthy
if (this.notifications) {
const errorMessage = error instanceof Error ? error.message : String(error);
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
await this.notifications.notify({
title: '🔥 Critical Error Captured',
message: `Error: ${errorMessage}${contextStr}`,
priority: 7,
});
}
return result;
}
captureMessage(message: string, level: ErrorReportingLevel = 'error') {

View File

@@ -1,11 +1,15 @@
import type { ErrorReportingLevel, ErrorReportingService, ErrorReportingUser } from './error-reporting-service';
import type {
ErrorReportingLevel,
ErrorReportingService,
ErrorReportingUser,
} from './error-reporting-service';
export class NoopErrorReportingService implements ErrorReportingService {
captureException(_error: unknown, _context?: Record<string, unknown>) {
async captureException(_error: unknown, _context?: Record<string, unknown>) {
return undefined;
}
captureMessage(_message: string, _level?: ErrorReportingLevel) {
async captureMessage(_message: string, _level?: ErrorReportingLevel) {
return undefined;
}

View File

@@ -12,20 +12,19 @@ export class PinoLoggerService implements LoggerService {
// In Next.js, especially in the Edge runtime or during instrumentation,
// pino transports (which use worker threads) can cause issues.
// We disable transport in production and during instrumentation.
const useTransport = !config.isProduction && typeof window === 'undefined';
const useTransport = config.isDevelopment && typeof window === 'undefined';
this.logger = pino({
name: name || 'app',
level: config.logging.level,
transport:
useTransport
? {
target: 'pino-pretty',
options: {
colorize: true,
},
}
: undefined,
transport: useTransport
? {
target: 'pino-pretty',
options: {
colorize: true,
},
}
: undefined,
});
}
}

View File

@@ -0,0 +1,49 @@
import { NotificationOptions, NotificationService } from './notification-service';
export interface GotifyConfig {
url: string;
token: string;
enabled: boolean;
}
export class GotifyNotificationService implements NotificationService {
constructor(private config: GotifyConfig) {}
async notify(options: NotificationOptions): Promise<void> {
if (!this.config.enabled) return;
try {
const { title, message, priority = 4 } = options;
const url = new URL('message', this.config.url);
url.searchParams.set('token', this.config.token);
const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
message,
priority,
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error('Gotify notification failed:', {
status: response.status,
error: errorText,
});
}
} catch (error) {
console.error('Gotify notification error:', error);
}
}
}
export class NoopNotificationService implements NotificationService {
async notify(): Promise<void> {
// Do nothing
}
}

View File

@@ -0,0 +1,9 @@
export interface NotificationOptions {
title: string;
message: string;
priority?: number;
}
export interface NotificationService {
notify(options: NotificationOptions): Promise<void>;
}

View File

@@ -191,7 +191,14 @@
"emailPlaceholder": "ihre@email.de",
"message": "Nachricht",
"messagePlaceholder": "Wie können wir Ihnen helfen?",
"submit": "Nachricht senden"
"submit": "Nachricht senden",
"submitting": "Wird gesendet...",
"successTitle": "Nachricht gesendet!",
"successDesc": "Vielen Dank für Ihre Nachricht. Wir werden uns so schnell wie möglich bei Ihnen melden.",
"sendAnother": "Weitere Nachricht senden",
"errorTitle": "Senden fehlgeschlagen!",
"error": "Etwas ist schief gelaufen. Bitte überprüfen Sie Ihre Eingaben und versuchen Sie es erneut.",
"tryAgain": "Erneut versuchen"
}
},
"Products": {

View File

@@ -191,7 +191,14 @@
"emailPlaceholder": "your@email.com",
"message": "Message",
"messagePlaceholder": "How can we help you?",
"submit": "Send Message"
"submit": "Send Message",
"submitting": "Sending...",
"successTitle": "Message Sent!",
"successDesc": "Thank you for your message. We will get back to you as soon as possible.",
"sendAnother": "Send another message",
"errorTitle": "Submission Failed!",
"error": "Something went wrong. Please check your input and try again.",
"tryAgain": "Try Again"
}
},
"Products": {

View File

@@ -0,0 +1,59 @@
import openpyxl
def update_excel_ampacity(file_path, headers_row_idx, ampacity_cols_identifiers, target_cross_section="1x1200/35"):
print(f"Updating {file_path}...")
wb = openpyxl.load_workbook(file_path)
ws = wb.active
# openpyxl is 1-indexed for rows and columns
headers = [cell.value for cell in ws[headers_row_idx]]
# Identify column indices for ampacity (0-indexed locally for easier row access)
col_indices = []
for identifier in ampacity_cols_identifiers:
if isinstance(identifier, int):
col_indices.append(identifier)
else:
try:
# list.index returns 0-indexed position
col_indices.append(headers.index(identifier))
except ValueError:
print(f"Warning: Could not find column '{identifier}' in {file_path}")
# Find row index for "Number of cores and cross-section" or use index 8
cs_col_idx = 8
try:
cs_col_idx = headers.index("Number of cores and cross-section")
except ValueError:
pass
rows_updated = 0
# ws.iter_rows returns 1-indexed rows
for row in ws.iter_rows(min_row=headers_row_idx + 1):
# row is a tuple of cells, so row[cs_col_idx] is 0-indexed access to the tuple
if str(row[cs_col_idx].value).strip() == target_cross_section:
for col_idx in col_indices:
row[col_idx].value = "On Request"
rows_updated += 1
wb.save(file_path)
print(f"Updated {rows_updated} rows in {file_path}")
# File 1: medium-voltage-KM.xlsx
update_excel_ampacity(
'data/excel/medium-voltage-KM.xlsx',
1, # Headers are in first row (1-indexed)
[
'Current ratings in air, trefoil*',
'Current ratings in air, flat*',
'Current ratings in ground, trefoil*',
'Current ratings in ground, flat*'
]
)
# File 2: medium-voltage-KM 170126.xlsx
update_excel_ampacity(
'data/excel/medium-voltage-KM 170126.xlsx',
1, # Indices 39 and 41 were from a 0-indexed JSON representation
[39, 41]
)

87
scripts/update_excel.py Normal file
View File

@@ -0,0 +1,87 @@
import openpyxl
excel_path = 'data/excel/medium-voltage-KM.xlsx'
wb = openpyxl.load_workbook(excel_path)
ws = wb.active
# Technical data for 1x1200RM/35
new_rows_data = [
{
"Rated voltage": "6/10",
"Test voltage": 21,
"Nominal insulation thickness": 3.4,
"Diameter over insulation (approx.)": 48.5,
"Minimum sheath thickness": 2.1,
"Outer diameter (approx.)": 59,
"Bending radius (min.)": 885,
"Weight (approx.)": 4800,
"Capacitance (approx.)": 0.95,
"Inductance, trefoil (approx.)": 0.24,
"Inductance in air, flat (approx.) 1": 0.40,
"Inductance in ground, flat (approx.) 1": 0.42,
},
{
"Rated voltage": "12/20",
"Test voltage": 42,
"Nominal insulation thickness": 5.5,
"Diameter over insulation (approx.)": 52.3,
"Minimum sheath thickness": 2.1,
"Outer diameter (approx.)": 66,
"Bending radius (min.)": 990,
"Weight (approx.)": 5200,
"Capacitance (approx.)": 1.05,
"Inductance, trefoil (approx.)": 0.23,
"Inductance in air, flat (approx.) 1": 0.43,
"Inductance in ground, flat (approx.) 1": 0.45,
},
{
"Rated voltage": "18/30",
"Test voltage": 63,
"Nominal insulation thickness": 8.0,
"Diameter over insulation (approx.)": 57.5,
"Minimum sheath thickness": 2.4,
"Outer diameter (approx.)": 71,
"Bending radius (min.)": 1065,
"Weight (approx.)": 5900,
"Capacitance (approx.)": 1.15,
"Inductance, trefoil (approx.)": 0.22,
"Inductance in air, flat (approx.) 1": 0.45,
"Inductance in ground, flat (approx.) 1": 0.47,
}
]
# Find a template row for NA2XS(F)2Y
template_row = None
headers = [cell.value for cell in ws[1]]
for row in ws.iter_rows(min_row=3, values_only=True):
if row[0] == 'NA2XS(F)2Y':
template_row = list(row)
break
if not template_row:
print("Error: Could not find template row for NA2XS(F)2Y")
exit(1)
# Function to update template with new values
def create_row(template, updates, headers):
new_row = template[:]
# Change "Number of cores and cross-section"
cs_idx = headers.index("Number of cores and cross-section")
new_row[cs_idx] = "1x1200/35"
# Apply specific updates
for key, value in updates.items():
if key in headers:
idx = headers.index(key)
new_row[idx] = value
return new_row
# Append new rows
for data in new_rows_data:
new_row_values = create_row(template_row, data, headers)
ws.append(new_row_values)
print(f"Added row for {data['Rated voltage']} kV")
wb.save(excel_path)
print("Excel file updated successfully.")

120
scripts/update_excel_v2.py Normal file
View File

@@ -0,0 +1,120 @@
import openpyxl
excel_path = 'data/excel/medium-voltage-KM 170126.xlsx'
wb = openpyxl.load_workbook(excel_path)
ws = wb.active
# Technical data for 1x1200RM/35
# Indices based on Row 2 (Units) and Row 1
# Index 0: Part Number
# Index 8: Querschnitt
# Index 9: Rated voltage
# Index 10: Test voltage
# Index 23: LD mm
# Index 24: ID mm
# Index 25: DI mm
# Index 26: MWD mm
# Index 27: AD mm
# Index 28: BR
# Index 29: G kg
# Index 30: RI Ohm
# Index 31: Cap
# Index 32: Inductance trefoil
# Index 35: BK
# Index 39: SBL 30
# Index 41: SBE 20
new_rows_data = [
{
"voltage": "6/10",
"test_v": 21,
"ld": 41.5,
"id": 3.4,
"di": 48.5,
"mwd": 2.1,
"ad": 59,
"br": 885,
"g": 4800,
"ri": 0.0247,
"cap": 0.95,
"ind": 0.24,
"bk": 113,
"sbl": 1300,
"sbe": 933
},
{
"voltage": "12/20",
"test_v": 42,
"ld": 41.5,
"id": 5.5,
"di": 52.3,
"mwd": 2.1,
"ad": 66,
"br": 990,
"g": 5200,
"ri": 0.0247,
"cap": 1.05,
"ind": 0.23,
"bk": 113,
"sbl": 1200,
"sbe": 900
},
{
"voltage": "18/30",
"test_v": 63,
"ld": 41.5,
"id": 8.0,
"di": 57.5,
"mwd": 2.4,
"ad": 71,
"br": 1065,
"g": 5900,
"ri": 0.0247,
"cap": 1.15,
"ind": 0.22,
"bk": 113,
"sbl": 1300,
"sbe": 950
}
]
# Find a template row for NA2XS(F)2Y
template_row = None
for row in ws.iter_rows(min_row=3, values_only=True):
if row[0] == 'NA2XS(F)2Y' and row[9] == '6/10':
template_row = list(row)
break
if not template_row:
print("Error: Could not find template row for NA2XS(F)2Y")
exit(1)
# Function to update template with new values
def create_row(template, data):
new_row = template[:]
new_row[8] = "1x1200/35"
new_row[9] = data["voltage"]
new_row[10] = data["test_v"]
new_row[23] = data["ld"]
new_row[24] = data["id"]
new_row[25] = data["di"]
new_row[26] = data["mwd"]
new_row[27] = data["ad"]
new_row[28] = data["br"]
new_row[29] = data["g"]
new_row[30] = data["ri"]
new_row[31] = data["cap"]
new_row[32] = data["ind"]
new_row[35] = data["bk"]
new_row[39] = data["sbl"]
new_row[41] = data["sbe"]
return new_row
# Append new rows
for data in new_rows_data:
new_row_values = create_row(template_row, data)
ws.append(new_row_values)
print(f"Added row for {data['voltage']} kV")
wb.save(excel_path)
print("Excel file updated successfully.")