commit 89bf57b6655bf16dc7ffd770081f8fe41e1d9db2 Author: Charles Date: Fri Apr 3 08:42:23 2026 +0200 Création du projet DaliACLI - CLI PowerShell pour la gestion des sorties DALI adressable (ECxLightDaliA) - Action Read : rapport CSV des configurations (une ligne par output) - Action Write : modification des propriétés via POST JSON - Fallback curl.exe -k pour les automates avec problèmes SSL diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6aefade --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/plan +*.log +*.csv \ No newline at end of file diff --git a/DaliACLI.ps1 b/DaliACLI.ps1 new file mode 100644 index 0000000..e8f895f --- /dev/null +++ b/DaliACLI.ps1 @@ -0,0 +1,235 @@ +<# +.SYNOPSIS + CLI DALI pour automates Distech Controls Eclypse. + Lecture et ecriture des configurations de sorties DALI adressable (ECxLightDaliA). + +.DESCRIPTION + DaliACLI permet de gerer les configurations DALI adressable sur des automates + Distech Controls Eclypse en utilisant leur API REST v2. + + Action READ : Lit la configuration des modules ECxLightDaliA de chaque automate + et genere un fichier CSV avec une ligne par sortie (output). + + Action WRITE : Modifie les proprietes des sorties DALI a partir du CSV. + Toutes les proprietes de chaque ligne sont reecrites. + +.PARAMETER Action + Action a effectuer : + Read - Lire la configuration et generer un CSV de sortie + Write - Ecrire les proprietes depuis le CSV vers les automates + +.PARAMETER CsvInput + Chemin vers le fichier CSV d'entree (separateur point-virgule). + Pour Read : colonnes minimales Hostname, Current Ip, HttpPort, HttpsPort. + Pour Write : CSV enrichi genere par l'action Read. + +.PARAMETER Username + Nom d'utilisateur pour l'authentification API (defaut: admin). + Peut etre surcharge par la colonne Username du CSV. + +.PARAMETER Password + Mot de passe pour l'authentification API (defaut: vide). + Peut etre surcharge par la colonne Password du CSV. + +.EXAMPLE + .\DaliACLI.ps1 -Action Read -CsvInput ".\automates.csv" + + Lit la configuration DALI de tous les automates listes dans le CSV. + Genere un fichier dali_YYYY-MM-DD_HHhMM.csv dans le repertoire courant. + +.EXAMPLE + .\DaliACLI.ps1 -Action Write -CsvInput ".\dali_2026-04-02_14h30.csv" -Password "MonMotDePasse" + + Ecrit les proprietes du CSV vers les automates. + +.EXAMPLE + Get-Help .\DaliACLI.ps1 -Detailed + + Affiche cette aide detaillee. + +.NOTES + Prerequis : PowerShell 5.1+ (inclus dans Windows 10/11) + API : Distech Controls Eclypse REST API v2 + Securite : TLS 1.2, certificats auto-signes acceptes +#> + +param( + [Parameter(Mandatory)] + [ValidateSet("Read", "Write")] + [string]$Action, + + [Parameter(Mandatory)] + [string]$CsvInput, + + [string]$Username = "admin", + + [string]$Password = "" +) + +# Performance : desactiver la barre de progression Invoke-WebRequest +$ProgressPreference = 'SilentlyContinue' + +# Import des modules +$modulesPath = Join-Path $PSScriptRoot "modules" +Import-Module (Join-Path $modulesPath "Logger.psm1") -Force +Import-Module (Join-Path $modulesPath "CsvHandler.psm1") -Force +Import-Module (Join-Path $modulesPath "ApiClient.psm1") -Force +Import-Module (Join-Path $modulesPath "DaliParser.psm1") -Force + +# Initialisation +Initialize-Logger +Initialize-ApiClient + +Write-Log -Message "=== DaliACLI demarre - Action: $Action ===" -Level INFO + +# Chemin de l'API DALI +$daliApiPath = "/api/rest/v2/services/subnet/devices/light-sunblind/modules" + +# Lecture du CSV d'entree +$csvRows = Read-AutomateCsv -CsvPath $CsvInput + +# ============================================================ +# ACTION READ : Lire la config DALI de chaque automate +# ============================================================ +if ($Action -eq "Read") { + $timestamp = Get-Date -Format "yyyy-MM-dd_HH\hmm" + $CsvOutput = Join-Path (Get-Location) "dali_$timestamp.csv" + Write-Log -Message "CSV de sortie : $CsvOutput" -Level INFO + + $allOutputRows = @() + + # Deduplication des automates par IP (le CSV d'entree a une ligne par automate) + $uniqueAutomates = $csvRows | Sort-Object -Property "Current Ip" -Unique + + foreach ($automate in $uniqueAutomates) { + $hostname = $automate.Hostname + $ip = $automate."Current Ip" + + Update-Stats -Counter AutomatesTotal + + try { + $baseUrl = Get-BaseUrl -Automate $automate + if (-not $baseUrl) { + Write-Log -Message "[$hostname] Aucun port HTTP/HTTPS valide - ignore" -Level WARN + Update-Stats -Counter AutomatesError + continue + } + + $creds = Get-Credentials -Automate $automate -DefaultUsername $Username -DefaultPassword $Password + $url = "$baseUrl$daliApiPath/" + + Write-Log -Message "[$hostname] $url (user: $($creds.Username))" -Level INFO + + # GET modules DALI + $jsonContent = Invoke-ApiGet -Url $url -Username $creds.Username -Password $creds.Password + + # Parser et filtrer les modules ECxLightDaliA + $daliOutputs = Get-DaliAModules -JsonContent $jsonContent + + if ($daliOutputs.Count -eq 0) { + Write-Log -Message "[$hostname] Aucun module ECxLightDaliA trouve" -Level WARN + continue + } + + Write-Log -Message "[$hostname] $($daliOutputs.Count) sortie(s) ECxLightDaliA trouvee(s)" -Level INFO + + # Creer une ligne CSV par output + foreach ($output in $daliOutputs) { + # Copier toutes les colonnes source de l'automate + $row = [ordered]@{} + foreach ($prop in $automate.PSObject.Properties) { + $row[$prop.Name] = $prop.Value + } + # Ajouter les colonnes DALI + $row["ModuleKey"] = $output.ModuleKey + $row["ModuleName"] = $output.ModuleName + $row["OutputKey"] = $output.OutputKey + $row["OutputName"] = $output.OutputName + $row["ControlGearKey"] = $output.ControlGearKey + $row["Groups"] = $output.Groups + $row["SystemFailLevel"] = $output.SystemFailLevel + $row["PowerOnLevel"] = $output.PowerOnLevel + $row["MinDimmingLevel"] = $output.MinDimmingLevel + $row["DefaultValue"] = $output.DefaultValue + $row["DimmingTime"] = $output.DimmingTime + $row["MaxDimmingLevel"] = $output.MaxDimmingLevel + + $allOutputRows += [PSCustomObject]$row + Update-Stats -Counter OutputsProcessed + + Write-Log -Message "[$hostname] M$($output.ModuleKey)_O$($output.OutputKey) ($($output.OutputName)) - Groups: $($output.Groups)" -Level SUCCESS + } + } + catch { + Write-Log -Message "[$hostname] ERREUR : $($_.Exception.Message)" -Level ERROR + Update-Stats -Counter AutomatesError + } + } + + # Ecriture du CSV de sortie + if ($allOutputRows.Count -gt 0) { + Write-OutputCsv -OutputRows $allOutputRows -OutputPath $CsvOutput + } + else { + Write-Log -Message "Aucune sortie trouvee - CSV non genere" -Level WARN + } +} + +# ============================================================ +# ACTION WRITE : Ecrire la config DALI sur chaque automate +# ============================================================ +elseif ($Action -eq "Write") { + # Grouper les lignes par automate (Current Ip) + $automateGroups = $csvRows | Group-Object -Property "Current Ip" + + foreach ($automateGroup in $automateGroups) { + $ip = $automateGroup.Name + $rows = $automateGroup.Group + $hostname = $rows[0].Hostname + + Update-Stats -Counter AutomatesTotal + + try { + $baseUrl = Get-BaseUrl -Automate $rows[0] + if (-not $baseUrl) { + Write-Log -Message "[$hostname] Aucun port HTTP/HTTPS valide - ignore" -Level WARN + Update-Stats -Counter AutomatesError + continue + } + + $creds = Get-Credentials -Automate $rows[0] -DefaultUsername $Username -DefaultPassword $Password + $url = "$baseUrl$daliApiPath" + + Write-Log -Message "[$hostname] $url (user: $($creds.Username))" -Level INFO + + # Grouper par ModuleKey pour envoyer un POST par module + $moduleGroups = $rows | Group-Object -Property ModuleKey + + foreach ($moduleGroup in $moduleGroups) { + $moduleKey = $moduleGroup.Name + $outputRows = $moduleGroup.Group + + Write-Log -Message "[$hostname] Module $moduleKey : $($outputRows.Count) sortie(s) a ecrire" -Level INFO + + # Construire le body JSON + $jsonBody = Build-ModuleWriteBody -ModuleKey $moduleKey -OutputRows $outputRows + + Write-Log -Message "[$hostname] Module $moduleKey body: $jsonBody" -Level INFO + + # POST vers l'API + Invoke-ApiPost -Url $url -JsonBody $jsonBody -Username $creds.Username -Password $creds.Password + + Update-Stats -Counter OutputsProcessed -Increment $outputRows.Count + + Write-Log -Message "[$hostname] Module $moduleKey : configuration ecrite avec succes" -Level SUCCESS + } + } + catch { + Write-Log -Message "[$hostname] ERREUR : $($_.Exception.Message)" -Level ERROR + Update-Stats -Counter AutomatesError + } + } +} + +# Resume final +Write-Summary diff --git a/README.md b/README.md new file mode 100644 index 0000000..c45624f --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# DaliACLI + +CLI PowerShell pour la gestion des configurations de sorties DALI adressable (**ECxLightDaliA**) sur des automates Distech Controls Eclypse. + +--- + +IMPORTANT : Ce projet est un développement personnel indépendant. + +Ce projet a été développé par Charles-Arthur DAVID à titre personnel +Distech Controls n'est pas responsable de ce projet et ne le supporte pas +Aucune demande de support ne sera prise en charge par Distech Controls +Distech Controls ne fournit aucune garantie ni assistance technique pour ce projet + +--- + +## Fonctionnalités + +- **Read** : Lit la configuration des modules ECxLightDaliA et génère un rapport CSV (une ligne par sortie) +- **Write** : Modifie les propriétés des sorties DALI à partir du CSV enrichi + +## Prérequis + +- PowerShell 5.1+ (inclus dans Windows 10/11) +- Accès réseau aux automates Eclypse + +## Utilisation + +### Lecture des configurations + +```powershell +.\DaliACLI.ps1 -Action Read -CsvInput ".\automates.csv" +``` + +Génère un fichier `dali_YYYY-MM-DD_HHhmm.csv` avec les propriétés de chaque sortie. + +### Écriture des configurations + +```powershell +.\DaliACLI.ps1 -Action Write -CsvInput ".\dali_2026-04-02_14h30.csv" +``` + +### Avec mot de passe + +```powershell +.\DaliACLI.ps1 -Action Read -CsvInput ".\automates.csv" -Password "MonMotDePasse" +``` + +### Aide + +```powershell +Get-Help .\DaliACLI.ps1 -Detailed +``` + +## Format CSV d'entrée (automates) + +Séparateur : point-virgule (`;`) + +```csv +Hostname;Current Ip;HttpPort;HttpsPort +Eclypse-RDC;192.168.1.11;0;443 +Eclypse-Etage1;192.168.1.12;0;443 +``` + +Colonnes optionnelles : `Username`, `Password` (surcharge les credentials par défaut `admin` / vide). + +## Format CSV de sortie (Read) + +Une ligne par sortie DALI : + +| Colonne | Description | +|---------|-------------| +| Hostname | Nom de l'automate | +| Current Ip | Adresse IP | +| HttpPort | Port HTTP | +| HttpsPort | Port HTTPS | +| ModuleKey | Identifiant du module | +| ModuleName | Nom du module | +| OutputKey | Identifiant de la sortie | +| OutputName | Nom de la sortie | +| ControlGearKey | Clé du control gear DALI | +| Groups | Groupes DALI actifs (ex: `1,3,5`) | +| SystemFailLevel | Niveau en cas de défaillance système | +| PowerOnLevel | Niveau à la mise sous tension | +| MinDimmingLevel | Niveau minimum de variation | +| DefaultValue | Valeur par défaut | +| DimmingTime | Temps de variation (secondes) | +| MaxDimmingLevel | Niveau maximum de variation | + +## Filtrage + +Seuls les modules de type **ECxLightDaliA** sont traités. Les autres types (ECxLight4Dali, etc.) sont ignorés automatiquement. + +## API + +- **Endpoint** : `/api/rest/v2/services/subnet/devices/light-sunblind/modules` +- **Auth** : Basic Auth +- **TLS 1.2** avec support des certificats auto-signés + +## Structure du projet + +``` +daliA/ +├── DaliACLI.ps1 # Script principal +├── modules/ +│ ├── Logger.psm1 # Logging console + fichier +│ ├── CsvHandler.psm1 # Lecture/écriture CSV +│ ├── ApiClient.psm1 # Communication REST API +│ └── DaliParser.psm1 # Parsing JSON DALI +├── plan/ +│ └── daliacli.md # Plan de développement +└── README.md +``` diff --git a/modules/ApiClient.psm1 b/modules/ApiClient.psm1 new file mode 100644 index 0000000..61a303e --- /dev/null +++ b/modules/ApiClient.psm1 @@ -0,0 +1,201 @@ +# Module ApiClient - Communication REST API Eclypse (API v2) + +$script:CurlAvailable = $false + +function Initialize-ApiClient { + [CmdletBinding()] + param() + + # Forcer TLS 1.2 + 1.1 + 1.0 + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls + + # Desactiver la verification SSL (certificats auto-signes) + if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) { + Add-Type @" +using System.Net; +using System.Security.Cryptography.X509Certificates; +public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } +} +"@ + } + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + + # Verifier si curl.exe est disponible (fallback SSL) + $script:CurlAvailable = [bool](Get-Command curl.exe -ErrorAction SilentlyContinue) + if ($script:CurlAvailable) { + Write-Log -Message "ApiClient initialise (TLS multi, SSL bypass, curl.exe fallback)" -Level INFO + } + else { + Write-Log -Message "ApiClient initialise (TLS multi, SSL bypass)" -Level INFO + } +} + +function Get-AuthHeader { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Password + ) + + $pair = "${Username}:${Password}" + $bytes = [System.Text.Encoding]::ASCII.GetBytes($pair) + $base64 = [System.Convert]::ToBase64String($bytes) + + return @{ + "Authorization" = "Basic $base64" + "Accept" = "application/json" + "User-Agent" = "DaliACLI/1.0 (PowerShell; Win32NT)" + } +} + +function Test-SslError { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [System.Exception]$Exception + ) + + $msg = $Exception.Message + return ($msg -match "SSL" -or $msg -match "TLS" -or $msg -match "confiance" -or $msg -match "trust" -or $msg -match "certificate") +} + +function Invoke-CurlGet { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Url, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Password + ) + + Write-Log -Message "GET $Url (curl -k fallback)" -Level WARN + + $output = & curl.exe -s -k -u "${Username}:${Password}" -H "Accept: application/json" $Url 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "curl GET erreur (exit code $LASTEXITCODE): $output" + } + + Write-Log -Message "GET OK (curl)" -Level SUCCESS + return ($output | Out-String) +} + +function Invoke-CurlPost { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Url, + + [Parameter(Mandatory)] + [string]$JsonBody, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Password + ) + + Write-Log -Message "POST $Url (curl -k fallback)" -Level WARN + + $tempFile = [System.IO.Path]::GetTempFileName() + try { + [System.IO.File]::WriteAllText($tempFile, $JsonBody, [System.Text.Encoding]::UTF8) + $output = & curl.exe -s -k -u "${Username}:${Password}" -H "Content-Type: application/json; charset=utf-8" -d "@$tempFile" $Url 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "curl POST erreur (exit code $LASTEXITCODE): $output" + } + Write-Log -Message "POST OK (curl)" -Level SUCCESS + } + finally { + Remove-Item $tempFile -Force -ErrorAction SilentlyContinue + } +} + +function Invoke-ApiGet { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Url, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Password + ) + + $headers = Get-AuthHeader -Username $Username -Password $Password + + Write-Log -Message "GET $Url" -Level INFO + + try { + $response = Invoke-WebRequest -Uri $Url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 30 + + if ($response.Content -is [byte[]]) { + return [System.Text.Encoding]::UTF8.GetString($response.Content).TrimStart([char]0xFEFF) + } + return $response.Content + } + catch { + if ((Test-SslError -Exception $_.Exception) -and $script:CurlAvailable) { + return Invoke-CurlGet -Url $Url -Username $Username -Password $Password + } + throw + } +} + +function Invoke-ApiPost { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Url, + + [Parameter(Mandatory)] + [string]$JsonBody, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Password + ) + + $headers = Get-AuthHeader -Username $Username -Password $Password + + Write-Log -Message "POST $Url" -Level INFO + + try { + $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($JsonBody) + $response = Invoke-WebRequest -Uri $Url -Method POST -Headers $headers -Body $bodyBytes -ContentType "application/json; charset=utf-8" -UseBasicParsing -TimeoutSec 30 + $statusCode = $response.StatusCode + Write-Log -Message "POST reponse : $statusCode" -Level SUCCESS + return $statusCode + } + catch { + if ((Test-SslError -Exception $_.Exception) -and $script:CurlAvailable) { + Invoke-CurlPost -Url $Url -JsonBody $JsonBody -Username $Username -Password $Password + return + } + Write-Log -Message "POST erreur : $($_.Exception.Message)" -Level ERROR + throw + } +} + +Export-ModuleMember -Function Initialize-ApiClient, Invoke-ApiGet, Invoke-ApiPost diff --git a/modules/CsvHandler.psm1 b/modules/CsvHandler.psm1 new file mode 100644 index 0000000..6ada391 --- /dev/null +++ b/modules/CsvHandler.psm1 @@ -0,0 +1,97 @@ +# Module CsvHandler - Lecture/ecriture CSV automates + +function Read-AutomateCsv { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$CsvPath + ) + + if (-not (Test-Path $CsvPath)) { + throw "Fichier CSV introuvable : $CsvPath" + } + + $rows = Import-Csv -Path $CsvPath -Delimiter ";" + Write-Log -Message "CSV charge : $($rows.Count) lignes depuis $CsvPath" -Level INFO + return $rows +} + +function Get-BaseUrl { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [PSCustomObject]$Automate + ) + + $ip = $Automate."Current Ip" + $httpsPort = $Automate.HttpsPort + $httpPort = $Automate.HttpPort + + # HTTPS si port > 0 et != -1 + if ($httpsPort -and $httpsPort -ne "" -and [int]$httpsPort -gt 0 -and [int]$httpsPort -ne -1) { + if ([int]$httpsPort -eq 443) { + return "https://$ip" + } + return "https://${ip}:$httpsPort" + } + + # HTTP si port > 0 + if ($httpPort -and $httpPort -ne "" -and [int]$httpPort -gt 0 -and [int]$httpPort -ne -1) { + if ([int]$httpPort -eq 80) { + return "http://$ip" + } + return "http://${ip}:$httpPort" + } + + return $null +} + +function Get-Credentials { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [PSCustomObject]$Automate, + + [string]$DefaultUsername = "admin", + [string]$DefaultPassword = "" + ) + + $username = $DefaultUsername + $password = $DefaultPassword + + # Override depuis le CSV si colonnes presentes et renseignees + $props = $Automate.PSObject.Properties.Name + if ("Username" -in $props -and $Automate.Username -and $Automate.Username -ne "") { + $username = $Automate.Username + } + if ("Password" -in $props -and $Automate.Password -and $Automate.Password -ne "") { + $password = $Automate.Password + } + + return @{ + Username = $username + Password = $password + } +} + +function Write-OutputCsv { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [array]$OutputRows, + + [Parameter(Mandatory)] + [string]$OutputPath + ) + + # Creer le repertoire de sortie si necessaire + $outputDir = Split-Path $OutputPath -Parent + if ($outputDir -and -not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Path $outputDir -Force | Out-Null + } + + $OutputRows | Export-Csv -Path $OutputPath -Delimiter ";" -NoTypeInformation -Encoding UTF8 + Write-Log -Message "CSV de sortie ecrit : $OutputPath ($($OutputRows.Count) lignes)" -Level SUCCESS +} + +Export-ModuleMember -Function Read-AutomateCsv, Get-BaseUrl, Get-Credentials, Write-OutputCsv diff --git a/modules/DaliParser.psm1 b/modules/DaliParser.psm1 new file mode 100644 index 0000000..1531bd8 --- /dev/null +++ b/modules/DaliParser.psm1 @@ -0,0 +1,124 @@ +# Module DaliParser - Parsing JSON modules DALI et construction body Write + +function Get-DaliAModules { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$JsonContent + ) + + $data = $JsonContent | ConvertFrom-Json + $results = @() + + foreach ($prop in $data.PSObject.Properties) { + $moduleKey = $prop.Name + $module = $prop.Value + + # Filtrer uniquement les modules ECxLightDaliA + if ($module."configured-module-type" -ne "ECxLightDaliA") { + continue + } + + $moduleName = $module.name + + foreach ($outProp in $module.outputs.PSObject.Properties) { + $outputKey = $outProp.Name + $output = $outProp.Value + + $results += [PSCustomObject][ordered]@{ + ModuleKey = $moduleKey + ModuleName = $moduleName + OutputKey = $outputKey + OutputName = $output.name + ControlGearKey = $output."control-gear-key" + Groups = (ConvertTo-GroupString -GroupArray $output."group-membership") + SystemFailLevel = $output."system-fail-level" + PowerOnLevel = $output."power-on-level" + MinDimmingLevel = $output."minimum-dimming-level" + DefaultValue = $output."default-value" + DimmingTime = $output."dimming-time" + MaxDimmingLevel = $output."maximum-dimming-level" + } + } + } + + return $results +} + +function ConvertTo-GroupString { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + $GroupArray + ) + + $activeGroups = @() + for ($i = 0; $i -lt $GroupArray.Count; $i++) { + if ($GroupArray[$i] -eq $true) { + $activeGroups += ($i + 1) + } + } + + return ($activeGroups -join ",") +} + +function ConvertFrom-GroupString { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$GroupString + ) + + $arr = @($false) * 16 + + if ($GroupString -and $GroupString.Trim() -ne "") { + $indices = $GroupString.Split(",") | ForEach-Object { [int]$_.Trim() } + foreach ($idx in $indices) { + if ($idx -ge 1 -and $idx -le 16) { + $arr[$idx - 1] = $true + } + } + } + + return , $arr +} + +function Build-ModuleWriteBody { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$ModuleKey, + + [Parameter(Mandatory)] + [array]$OutputRows + ) + + $outputs = @{} + + foreach ($row in $OutputRows) { + $outputKey = $row.OutputKey + $groupArray = ConvertFrom-GroupString -GroupString $row.Groups + + $outputs[$outputKey] = [ordered]@{ + "control-gear-key" = [string]$row.ControlGearKey + "group-membership" = $groupArray + "system-fail-level" = [double]$row.SystemFailLevel + "power-on-level" = [double]$row.PowerOnLevel + "minimum-dimming-level" = [double]$row.MinDimmingLevel + "default-value" = [double]$row.DefaultValue + "dimming-time" = [double]$row.DimmingTime + "maximum-dimming-level" = [double]$row.MaxDimmingLevel + } + } + + $body = @{ + $ModuleKey = @{ + "outputs" = $outputs + } + } + + return ($body | ConvertTo-Json -Depth 10) +} + +Export-ModuleMember -Function Get-DaliAModules, ConvertTo-GroupString, ConvertFrom-GroupString, Build-ModuleWriteBody diff --git a/modules/Logger.psm1 b/modules/Logger.psm1 new file mode 100644 index 0000000..021af44 --- /dev/null +++ b/modules/Logger.psm1 @@ -0,0 +1,95 @@ +# Module Logger - Gestion des logs console + fichier + +$script:LogFile = $null +$script:Stopwatch = $null +$script:Stats = @{ + AutomatesTotal = 0 + AutomatesError = 0 + OutputsProcessed = 0 +} + +function Initialize-Logger { + [CmdletBinding()] + param() + + $timestamp = Get-Date -Format "yyyy-MM-dd_HH\hmm" + $script:LogFile = Join-Path (Get-Location) "dali_$timestamp.log" + New-Item -ItemType File -Path $script:LogFile -Force | Out-Null + + $script:Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + + $script:Stats = @{ + AutomatesTotal = 0 + AutomatesError = 0 + OutputsProcessed = 0 + } + + Write-Log -Message "Logger initialise - fichier: $($script:LogFile)" -Level INFO +} + +function Write-Log { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Message, + + [ValidateSet("INFO", "WARN", "ERROR", "SUCCESS")] + [string]$Level = "INFO" + ) + + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $line = "[$timestamp] [$Level] $Message" + + # Couleur console selon le niveau + switch ($Level) { + "INFO" { Write-Host $line -ForegroundColor Cyan } + "WARN" { Write-Host $line -ForegroundColor Yellow } + "ERROR" { Write-Host $line -ForegroundColor Red } + "SUCCESS" { Write-Host $line -ForegroundColor Green } + } + + # Ecriture fichier + if ($script:LogFile) { + Add-Content -Path $script:LogFile -Value $line -Encoding UTF8 + } +} + +function Update-Stats { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet("AutomatesTotal", "AutomatesError", "OutputsProcessed")] + [string]$Counter, + + [int]$Increment = 1 + ) + + $script:Stats[$Counter] += $Increment +} + +function Write-Summary { + [CmdletBinding()] + param() + + $script:Stopwatch.Stop() + $duration = $script:Stopwatch.Elapsed + + Write-Log -Message "========== RESUME ==========" -Level INFO + Write-Log -Message "Automates traites : $($script:Stats.AutomatesTotal)" -Level INFO + Write-Log -Message "Automates en erreur : $($script:Stats.AutomatesError)" -Level $(if ($script:Stats.AutomatesError -gt 0) { "WARN" } else { "INFO" }) + Write-Log -Message "Outputs traites : $($script:Stats.OutputsProcessed)" -Level INFO + Write-Log -Message "Duree totale : $($duration.ToString('hh\:mm\:ss\.ff'))" -Level INFO + Write-Log -Message "============================" -Level INFO +} + +function Get-LogDirectory { + [CmdletBinding()] + param() + + if ($script:LogFile) { + return Split-Path $script:LogFile -Parent + } + return $null +} + +Export-ModuleMember -Function Initialize-Logger, Write-Log, Update-Stats, Write-Summary, Get-LogDirectory