From c75c731ddc09a0e63c347320715a6868c6c19014 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 3 Mar 2026 23:26:20 +0100 Subject: [PATCH] =?UTF-8?q?Impl=C3=A9mentation=20CLI=20PowerShell=20Enocea?= =?UTF-8?q?n=20Eclypse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + EnoceanCLI.ps1 | 214 ++++++++++++++++++++++++++++++++++++++++ modules/ApiClient.psm1 | 146 +++++++++++++++++++++++++++ modules/CsvHandler.psm1 | 155 +++++++++++++++++++++++++++++ modules/Logger.psm1 | 95 ++++++++++++++++++ modules/XmlParser.psm1 | 74 ++++++++++++++ modules/ZipBuilder.psm1 | 54 ++++++++++ 7 files changed, 740 insertions(+) create mode 100644 .gitignore create mode 100644 EnoceanCLI.ps1 create mode 100644 modules/ApiClient.psm1 create mode 100644 modules/CsvHandler.psm1 create mode 100644 modules/Logger.psm1 create mode 100644 modules/XmlParser.psm1 create mode 100644 modules/ZipBuilder.psm1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9054bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/plan +/.idea diff --git a/EnoceanCLI.ps1 b/EnoceanCLI.ps1 new file mode 100644 index 0000000..67120ed --- /dev/null +++ b/EnoceanCLI.ps1 @@ -0,0 +1,214 @@ +# EnoceanCLI.ps1 - CLI Enocean pour automates Distech Controls Eclypse +# Lecture/ecriture des configurations Enocean via REST API + +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 "XmlParser.psm1") -Force +Import-Module (Join-Path $modulesPath "ApiClient.psm1") -Force +Import-Module (Join-Path $modulesPath "ZipBuilder.psm1") -Force + +# Initialisation +Initialize-Logger +Initialize-ApiClient + +Write-Log -Message "=== EnoceanCLI demarre - Action: $Action ===" -Level INFO + +# Lecture du CSV d'entree +$automates = Read-AutomateCsv -CsvPath $CsvInput + +# ============================================================ +# ACTION READ : Lire la config Enocean de chaque automate +# ============================================================ +if ($Action -eq "Read") { + # Chemin CSV de sortie automatique dans le repertoire courant + $timestamp = Get-Date -Format "yyyy-MM-dd_HH\hmm" + $CsvOutput = Join-Path (Get-Location) "enocean_$timestamp.csv" + Write-Log -Message "CSV de sortie : $CsvOutput" -Level INFO + + # Stockage des devices par IP : @{ "10.60.x.x" = @( @{DeviceId=...; DeviceType=...}, ... ) } + $deviceData = @{} + + foreach ($automate in $automates) { + $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 + } + + $apiBasePath = Get-ApiBasePath -Automate $automate + $creds = Get-Credentials -Automate $automate -DefaultUsername $Username -DefaultPassword $Password + + Write-Log -Message "[$hostname] $baseUrl$apiBasePath (user: $($creds.Username))" -Level INFO + + # GET liste des devices Enocean + $jsonContent = Invoke-EnoceanGet ` + -BaseUrl $baseUrl ` + -ApiBasePath $apiBasePath ` + -ResourcePath "files/enocean/configuration/devices" ` + -Username $creds.Username ` + -Password $creds.Password + + $deviceFiles = Get-DeviceListFromJson -JsonContent $jsonContent + + if ($deviceFiles.Count -eq 0) { + Write-Log -Message "[$hostname] Aucun device Enocean trouve" -Level WARN + $deviceData[$ip] = @() + continue + } + + Write-Log -Message "[$hostname] $($deviceFiles.Count) device(s) trouve(s)" -Level INFO + + # GET chaque XML device et parser + $devices = @() + foreach ($deviceFile in $deviceFiles) { + try { + $xmlContent = Invoke-EnoceanGet ` + -BaseUrl $baseUrl ` + -ApiBasePath $apiBasePath ` + -ResourcePath "files/enocean/configuration/devices/$($deviceFile.Name)?encode=bin" ` + -Username $creds.Username ` + -Password $creds.Password + + $parsed = Parse-EnoceanDeviceXml -XmlContent $xmlContent + $devices += $parsed + Update-Stats -Counter DevicesProcessed + + Write-Log -Message "[$hostname] Device $($deviceFile.Name) : Id=$($parsed.DeviceId), Type=$($parsed.DeviceType)" -Level SUCCESS + } + catch { + Write-Log -Message "[$hostname] Erreur lecture $($deviceFile.Name) : $($_.Exception.Message)" -Level ERROR + } + } + + # Trier par ResourceNumber puis stocker + $devices = @($devices | Sort-Object { $_.ResourceNumber }) + $deviceData[$ip] = $devices + } + catch { + Write-Log -Message "[$hostname] ERREUR : $($_.Exception.Message)" -Level ERROR + Update-Stats -Counter AutomatesError + $deviceData[$ip] = @() + } + } + + # Ecriture du CSV de sortie + Write-OutputCsv -InputRows $automates -DeviceData $deviceData -OutputPath $CsvOutput +} + +# ============================================================ +# ACTION WRITE : Ecrire la config Enocean sur chaque automate +# ============================================================ +elseif ($Action -eq "Write") { + foreach ($automate in $automates) { + $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 + } + + $apiBasePath = Get-ApiBasePath -Automate $automate + $creds = Get-Credentials -Automate $automate -DefaultUsername $Username -DefaultPassword $Password + + Write-Log -Message "[$hostname] $baseUrl$apiBasePath (user: $($creds.Username))" -Level INFO + + # Lire les colonnes dynamiques DeviceId_N / DeviceType_N + $xmlFiles = @() + $resourceIndex = 1 + + while ($true) { + $deviceIdCol = "DeviceId_$resourceIndex" + $deviceTypeCol = "DeviceType_$resourceIndex" + + # Verifier si les colonnes existent + $props = $automate.PSObject.Properties.Name + if ($deviceIdCol -notin $props -or $deviceTypeCol -notin $props) { + break + } + + $deviceId = $automate.$deviceIdCol + $deviceType = $automate.$deviceTypeCol + + # Ignorer si vide + if (-not $deviceId -or $deviceId -eq "" -or -not $deviceType -or $deviceType -eq "") { + $resourceIndex++ + continue + } + + $xmlContent = New-EnoceanDeviceXml ` + -ResourceNumber $resourceIndex ` + -DeviceId $deviceId ` + -DeviceType $deviceType + + $xmlFiles += @{ + Name = "enoceandevice$resourceIndex.xml" + Content = $xmlContent + } + + Update-Stats -Counter DevicesProcessed + Write-Log -Message "[$hostname] XML genere : device $resourceIndex (Id=$deviceId, Type=$deviceType)" -Level INFO + + $resourceIndex++ + } + + if ($xmlFiles.Count -eq 0) { + Write-Log -Message "[$hostname] Aucun device a ecrire - ignore" -Level WARN + continue + } + + # Creer le ZIP et l'envoyer + $zipBytes = New-EnoceanZip -XmlFiles $xmlFiles + $zipFilename = Get-ZipFilename -ZipBytes $zipBytes + + Write-Log -Message "[$hostname] Envoi de $($xmlFiles.Count) device(s) ($zipFilename)..." -Level INFO + + Send-EnoceanConfig ` + -BaseUrl $baseUrl ` + -ApiBasePath $apiBasePath ` + -ZipBytes $zipBytes ` + -ZipFilename $zipFilename ` + -Username $creds.Username ` + -Password $creds.Password + + Write-Log -Message "[$hostname] Configuration envoyee 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/modules/ApiClient.psm1 b/modules/ApiClient.psm1 new file mode 100644 index 0000000..705ac84 --- /dev/null +++ b/modules/ApiClient.psm1 @@ -0,0 +1,146 @@ +# Module ApiClient - Communication REST API Eclypse + +function Initialize-ApiClient { + [CmdletBinding()] + param() + + # Forcer TLS 1.2 + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + # 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 + + Write-Log -Message "ApiClient initialise (TLS 1.2, 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, text/json, text/x-json, text/javascript, application/xml, text/xml, text/plain" + "User-Agent" = "EC-gfxProgram/7.9.26006.1 (DC_API:v1; Win32NT 6.2)" + } +} + +function Invoke-EnoceanGet { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$BaseUrl, + + [Parameter(Mandatory)] + [string]$ApiBasePath, + + [Parameter(Mandatory)] + [string]$ResourcePath, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Password + ) + + $headers = Get-AuthHeader -Username $Username -Password $Password + $url = "$BaseUrl$ApiBasePath/$($ResourcePath.TrimStart('/'))" + + Write-Log -Message "GET $url" -Level INFO + + $response = Invoke-WebRequest -Uri $url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 30 + + # Si la reponse est un byte[] (encode=bin), decoder en string UTF-8 + if ($response.Content -is [byte[]]) { + return [System.Text.Encoding]::UTF8.GetString($response.Content) + } + return $response.Content +} + +function Send-EnoceanConfig { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$BaseUrl, + + [Parameter(Mandatory)] + [string]$ApiBasePath, + + [Parameter(Mandatory)] + [byte[]]$ZipBytes, + + [Parameter(Mandatory)] + [string]$ZipFilename, + + [Parameter(Mandatory)] + [string]$Username, + + [Parameter(Mandatory)] + [AllowEmptyString()] + [string]$Password + ) + + $headers = Get-AuthHeader -Username $Username -Password $Password + $url = "$BaseUrl$ApiBasePath/files/bacnet/inputConfiguration" + + Write-Log -Message "POST $url (fichier: $ZipFilename, taille: $($ZipBytes.Length) octets)" -Level INFO + + # Construction multipart manuelle (compatible PS 5.1, pas de -Form) + $boundary = [System.Guid]::NewGuid().ToString("N") + $headers["Content-Type"] = "multipart/form-data; boundary=$boundary" + + $encoding = [System.Text.Encoding]::ASCII + + # Partie avant le fichier + $headerPart = @" +--$boundary +Content-Disposition: form-data; name="File"; filename="$ZipFilename" +Content-Type: application/octet-stream + +"@ + # Partie finale + $footerPart = "`r`n--$boundary--`r`n" + + $headerBytes = $encoding.GetBytes($headerPart.Replace("`n", "`r`n")) + $footerBytes = $encoding.GetBytes($footerPart) + + # Assembler le body complet en byte[] + $bodyStream = New-Object System.IO.MemoryStream + $bodyStream.Write($headerBytes, 0, $headerBytes.Length) + $bodyStream.Write($ZipBytes, 0, $ZipBytes.Length) + $bodyStream.Write($footerBytes, 0, $footerBytes.Length) + $bodyBytes = $bodyStream.ToArray() + $bodyStream.Close() + + $response = Invoke-WebRequest -Uri $url -Method POST -Headers $headers -Body $bodyBytes -UseBasicParsing -TimeoutSec 60 + + Write-Log -Message "POST reponse : $($response.StatusCode)" -Level SUCCESS + return $response +} + +Export-ModuleMember -Function Initialize-ApiClient, Get-AuthHeader, Invoke-EnoceanGet, Send-EnoceanConfig diff --git a/modules/CsvHandler.psm1 b/modules/CsvHandler.psm1 new file mode 100644 index 0000000..a237594 --- /dev/null +++ b/modules/CsvHandler.psm1 @@ -0,0 +1,155 @@ +# 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-ApiBasePath { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [PSCustomObject]$Automate + ) + + $restUrl = $Automate.RestServiceURL + if ($restUrl -and $restUrl -ne "") { + return $restUrl.TrimEnd("/") + } + + # Fallback v1 + return "/api/rest/v1" +} + +function Get-Credentials { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [PSCustomObject]$Automate, + + [string]$DefaultUsername = "admin", + [string]$DefaultPassword = "" + ) + + $username = $DefaultUsername + $password = $DefaultPassword + + # Override depuis le CSV si renseigne + if ($Automate.Username -and $Automate.Username -ne "") { + $username = $Automate.Username + } + if ($Automate.Password -and $Automate.Password -ne "") { + $password = $Automate.Password + } + + return @{ + Username = $username + Password = $password + } +} + +function Write-OutputCsv { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [array]$InputRows, + + [Parameter(Mandatory)] + [hashtable]$DeviceData, + + [Parameter(Mandatory)] + [string]$OutputPath + ) + + # Trouver le nombre max de devices parmi tous les automates + $maxDevices = 0 + foreach ($key in $DeviceData.Keys) { + $count = $DeviceData[$key].Count + if ($count -gt $maxDevices) { + $maxDevices = $count + } + } + + Write-Log -Message "Max devices par automate : $maxDevices" -Level INFO + + # Construire les lignes de sortie + $outputRows = @() + foreach ($row in $InputRows) { + # Copier toutes les proprietes existantes + $obj = [ordered]@{} + foreach ($prop in $row.PSObject.Properties) { + $obj[$prop.Name] = $prop.Value + } + + # Ajouter les colonnes dynamiques DeviceId_N / DeviceType_N + $ip = $row."Current Ip" + $devices = @() + if ($DeviceData.ContainsKey($ip)) { + $devices = $DeviceData[$ip] + } + + for ($i = 1; $i -le $maxDevices; $i++) { + if ($i -le $devices.Count) { + $obj["DeviceId_$i"] = $devices[$i - 1].DeviceId + $obj["DeviceType_$i"] = $devices[$i - 1].DeviceType + } else { + $obj["DeviceId_$i"] = "" + $obj["DeviceType_$i"] = "" + } + } + + $outputRows += [PSCustomObject]$obj + } + + # 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-ApiBasePath, Get-Credentials, Write-OutputCsv diff --git a/modules/Logger.psm1 b/modules/Logger.psm1 new file mode 100644 index 0000000..68c8e18 --- /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 + DevicesProcessed = 0 +} + +function Initialize-Logger { + [CmdletBinding()] + param() + + $timestamp = Get-Date -Format "yyyy-MM-dd_HH\hmm" + $script:LogFile = Join-Path (Get-Location) "enocean_$timestamp.log" + New-Item -ItemType File -Path $script:LogFile -Force | Out-Null + + $script:Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + + $script:Stats = @{ + AutomatesTotal = 0 + AutomatesError = 0 + DevicesProcessed = 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", "DevicesProcessed")] + [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 "Devices traites : $($script:Stats.DevicesProcessed)" -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 diff --git a/modules/XmlParser.psm1 b/modules/XmlParser.psm1 new file mode 100644 index 0000000..0f2894f --- /dev/null +++ b/modules/XmlParser.psm1 @@ -0,0 +1,74 @@ +# Module XmlParser - Parse et generation XML Enocean + +function Get-DeviceListFromJson { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$JsonContent + ) + + $json = $JsonContent | ConvertFrom-Json + + $devices = @() + if ($json.files) { + foreach ($file in $json.files) { + $devices += @{ + Name = $file.path.name + Href = $file.path.href + } + } + } + + return , $devices +} + +function Parse-EnoceanDeviceXml { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$XmlContent + ) + + [xml]$xml = $XmlContent + $config = $xml.EnOceanDevice.Configuration + + return @{ + ResourceNumber = [int]$config.ResourceNumber + DeviceId = $config.DeviceId + DeviceType = $config.DeviceType + } +} + +function New-EnoceanDeviceXml { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [int]$ResourceNumber, + + [Parameter(Mandatory)] + [string]$DeviceId, + + [Parameter(Mandatory)] + [string]$DeviceType + ) + + $xml = @" + + + + EnOceanDevice + $ResourceNumber + EnOcean Device $ResourceNumber + $DeviceId + $DeviceType + 2400 + + + + +"@ + + return $xml +} + +Export-ModuleMember -Function Get-DeviceListFromJson, Parse-EnoceanDeviceXml, New-EnoceanDeviceXml diff --git a/modules/ZipBuilder.psm1 b/modules/ZipBuilder.psm1 new file mode 100644 index 0000000..88349d3 --- /dev/null +++ b/modules/ZipBuilder.psm1 @@ -0,0 +1,54 @@ +# Module ZipBuilder - Creation ZIP en memoire pour envoi config Enocean + +Add-Type -AssemblyName System.IO.Compression + +function New-EnoceanZip { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [hashtable[]]$XmlFiles # Chaque hashtable : @{ Name = "enoceandevice1.xml"; Content = "..." } + ) + + $memoryStream = New-Object System.IO.MemoryStream + + # leaveOpen = $true pour pouvoir lire le stream apres fermeture de l'archive + $archive = New-Object System.IO.Compression.ZipArchive($memoryStream, [System.IO.Compression.ZipArchiveMode]::Create, $true) + + foreach ($xmlFile in $XmlFiles) { + # Chemin interne avec forward slashes + $entryPath = "enocean/configuration/devices/$($xmlFile.Name)" + $entry = $archive.CreateEntry($entryPath, [System.IO.Compression.CompressionLevel]::Optimal) + + $entryStream = $entry.Open() + $writer = New-Object System.IO.StreamWriter($entryStream, [System.Text.Encoding]::UTF8) + $writer.Write($xmlFile.Content) + $writer.Flush() + $writer.Close() + $entryStream.Close() + } + + $archive.Dispose() + + $zipBytes = $memoryStream.ToArray() + $memoryStream.Close() + + Write-Log -Message "ZIP cree en memoire : $($XmlFiles.Count) fichier(s), $($zipBytes.Length) octets" -Level INFO + + return $zipBytes +} + +function Get-ZipFilename { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [byte[]]$ZipBytes + ) + + $md5 = [System.Security.Cryptography.MD5]::Create() + $hashBytes = $md5.ComputeHash($ZipBytes) + $hashString = ($hashBytes | ForEach-Object { $_.ToString("x2") }) -join "" + + return "fullConfig.$hashString.zip" +} + +Export-ModuleMember -Function New-EnoceanZip, Get-ZipFilename