From bd4fde81905cbdeca2d359b986dc97b111b5a5ac Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 5 Mar 2026 10:36:52 +0100 Subject: [PATCH] Correction README.md Ajout modification du fichier Project.gfx --- EnoceanCLI.ps1 | 45 ++++++++++++++++++++++++-- README.md | 8 ++--- modules/ApiClient.psm1 | 66 ++++++++++++++++++++++++++++++------- modules/XmlParser.psm1 | 32 +++++++++++++++++- modules/ZipBuilder.psm1 | 72 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 204 insertions(+), 19 deletions(-) diff --git a/EnoceanCLI.ps1 b/EnoceanCLI.ps1 index 8d91e6d..fd668f9 100644 --- a/EnoceanCLI.ps1 +++ b/EnoceanCLI.ps1 @@ -244,6 +244,7 @@ elseif ($Action -eq "Write") { # Associer par index sequentiel : device[0] -> DeviceId_1, device[1] -> DeviceId_2, etc. $xmlFiles = @() + $deviceIdMap = @{} # Paires ancien -> nouveau pour mise a jour GFx for ($i = 0; $i -lt $existingDevices.Count; $i++) { $seqIndex = $i + 1 $deviceIdCol = "DeviceId_$seqIndex" @@ -281,6 +282,11 @@ elseif ($Action -eq "Write") { Content = $result.ModifiedXml } + # Collecter la paire ancien -> nouveau pour le GFx + if ($result.OldDeviceId -ne $newDeviceId) { + $deviceIdMap[$result.OldDeviceId] = $newDeviceId + } + Update-Stats -Counter DevicesProcessed Write-Log -Message "[$hostname] Device $($device.Name) : $($result.OldDeviceId) -> $newDeviceId" -Level INFO } @@ -290,7 +296,7 @@ elseif ($Action -eq "Write") { continue } - # Creer le ZIP et l'envoyer + # Creer le ZIP enocean et l'envoyer $zipBytes = New-EnoceanZip -XmlFiles $xmlFiles $zipFilename = Get-ZipFilename -ZipBytes $zipBytes @@ -304,7 +310,42 @@ elseif ($Action -eq "Write") { -Username $creds.Username ` -Password $creds.Password - Write-Log -Message "[$hostname] Configuration envoyee avec succes" -Level SUCCESS + Write-Log -Message "[$hostname] Configuration enocean envoyee avec succes" -Level SUCCESS + + # Mise a jour du Project.gfx (fichier source du programme) + if ($deviceIdMap.Count -gt 0) { + Write-Log -Message "[$hostname] Mise a jour du Project.gfx..." -Level INFO + + $gfxBytes = Invoke-EnoceanGet ` + -BaseUrl $baseUrl ` + -ApiBasePath $apiBasePath ` + -ResourcePath "files/common/localDevice/project/Project.gfx?encode=bin" ` + -Username $creds.Username ` + -Password $creds.Password ` + -RawBytes + + $mainXml = Read-GfxMainXml -GfxBytes $gfxBytes + + $gfxResult = Update-GfxDeviceIds -MainXmlContent $mainXml -DeviceIdMap $deviceIdMap + + if ($gfxResult.ReplaceCount -gt 0) { + $newGfxBytes = Update-GfxZip -GfxBytes $gfxBytes -ModifiedMainXml $gfxResult.ModifiedXml + + Send-MultipartFile ` + -BaseUrl $baseUrl ` + -ApiBasePath $apiBasePath ` + -ResourcePath "files/common/localDevice/project" ` + -FileBytes $newGfxBytes ` + -Filename "Project.gfx" ` + -Username $creds.Username ` + -Password $creds.Password + + Write-Log -Message "[$hostname] Project.gfx mis a jour ($($gfxResult.ReplaceCount) DeviceId modifie(s))" -Level SUCCESS + } + else { + Write-Log -Message "[$hostname] Project.gfx : aucun DeviceId trouve a modifier" -Level WARN + } + } } catch { Write-Log -Message "[$hostname] ERREUR : $($_.Exception.Message)" -Level ERROR diff --git a/README.md b/README.md index 2643bfb..0bc9486 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Avec un mot de passe : ### Action Write — Écrire les DeviceId sur les automates ```powershell -.\EnoceanCLI.ps1 -Action Write -CsvInput ".\enocean_2026-03-03_23h07.csv" -Password "MonMotDePasse" +.\EnoceanCLI.ps1 -Action Write -CsvInput ".\enocean_2026-03-04_10h07.csv" -Password "MonMotDePasse" ``` **Ce qui se passe** : @@ -194,7 +194,7 @@ Relancer un Read pour confirmer que les DeviceId ont bien changé : Chaque exécution génère un fichier log dans le dossier courant : ``` -enocean_2026-03-04_14h30.log +enocean_2026-03-04_15h30.log ``` ### Niveaux de log @@ -210,7 +210,7 @@ enocean_2026-03-04_14h30.log ``` [2026-03-04 00:17:43] [INFO] === EnoceanCLI demarre - Action: Write === -[2026-03-04 00:17:43] [INFO] [MON-AUTOMATE-01] https://10.60.105.42/api/rest/v1 (user: admin) +[2026-03-04 00:17:43] [INFO] [MON-AUTOMATE-01] https://192.168.1.11/api/rest/v1 (user: admin) [2026-03-04 00:17:43] [INFO] [MON-AUTOMATE-01] 3 device(s) existant(s) sur l'automate [2026-03-04 00:17:43] [INFO] [MON-AUTOMATE-01] Device enoceandevice1.xml : 12345678 -> 99864513 [2026-03-04 00:17:43] [INFO] [MON-AUTOMATE-01] Device enoceandevice2.xml : 87654321 -> 65313272 @@ -260,7 +260,7 @@ L'automate n'a pas de capteurs EnOcean configurés. L'action Write ne peut pas m ### Timeout ou erreur de connexion -- Vérifier que l'automate est joignable (`ping 10.60.x.x`) +- Vérifier que l'automate est joignable (`ping 192.168.1.x`) - Vérifier les identifiants (Username / Password) - Vérifier que le port est correct (HTTP 80 ou HTTPS 443) diff --git a/modules/ApiClient.psm1 b/modules/ApiClient.psm1 index 08bb897..884469c 100644 --- a/modules/ApiClient.psm1 +++ b/modules/ApiClient.psm1 @@ -65,7 +65,9 @@ function Invoke-EnoceanGet { [Parameter(Mandatory)] [AllowEmptyString()] - [string]$Password + [string]$Password, + + [switch]$RawBytes ) $headers = Get-AuthHeader -Username $Username -Password $Password @@ -75,6 +77,14 @@ function Invoke-EnoceanGet { $response = Invoke-WebRequest -Uri $url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 30 + # Retourner les bytes bruts si demande (pour fichiers binaires comme .gfx) + if ($RawBytes) { + if ($response.Content -is [byte[]]) { + return , $response.Content + } + return , [System.Text.Encoding]::UTF8.GetBytes($response.Content) + } + # Si la reponse est un byte[] (encode=bin), decoder en string UTF-8 sans BOM if ($response.Content -is [byte[]]) { return [System.Text.Encoding]::UTF8.GetString($response.Content).TrimStart([char]0xFEFF) @@ -82,7 +92,7 @@ function Invoke-EnoceanGet { return $response.Content } -function Send-EnoceanConfig { +function Send-MultipartFile { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -92,10 +102,13 @@ function Send-EnoceanConfig { [string]$ApiBasePath, [Parameter(Mandatory)] - [byte[]]$ZipBytes, + [string]$ResourcePath, [Parameter(Mandatory)] - [string]$ZipFilename, + [byte[]]$FileBytes, + + [Parameter(Mandatory)] + [string]$Filename, [Parameter(Mandatory)] [string]$Username, @@ -106,17 +119,15 @@ function Send-EnoceanConfig { ) $authHeaders = Get-AuthHeader -Username $Username -Password $Password - $url = "$BaseUrl$ApiBasePath/files/bacnet/inputConfiguration" + $url = "$BaseUrl$ApiBasePath/$($ResourcePath.TrimStart('/'))" - Write-Log -Message "POST $url (fichier: $ZipFilename, taille: $($ZipBytes.Length) octets)" -Level INFO + Write-Log -Message "POST $url (fichier: $Filename, taille: $($FileBytes.Length) octets)" -Level INFO # Construction multipart manuelle $boundary = [System.Guid]::NewGuid().ToString("N") $encoding = [System.Text.Encoding]::ASCII - # Partie avant le fichier - $headerPart = "--$boundary`r`nContent-Disposition: form-data; name=`"File`"; filename=`"$ZipFilename`"`r`nContent-Type: application/octet-stream`r`n`r`n" - # Partie finale + $headerPart = "--$boundary`r`nContent-Disposition: form-data; name=`"File`"; filename=`"$Filename`"`r`nContent-Type: application/octet-stream`r`n`r`n" $footerPart = "`r`n--$boundary--`r`n" $headerBytes = $encoding.GetBytes($headerPart) @@ -125,7 +136,7 @@ function Send-EnoceanConfig { # 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($FileBytes, 0, $FileBytes.Length) $bodyStream.Write($footerBytes, 0, $footerBytes.Length) $bodyBytes = $bodyStream.ToArray() $bodyStream.Close() @@ -165,4 +176,37 @@ function Send-EnoceanConfig { } } -Export-ModuleMember -Function Initialize-ApiClient, Get-AuthHeader, Invoke-EnoceanGet, Send-EnoceanConfig +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 + ) + + Send-MultipartFile ` + -BaseUrl $BaseUrl ` + -ApiBasePath $ApiBasePath ` + -ResourcePath "files/bacnet/inputConfiguration" ` + -FileBytes $ZipBytes ` + -Filename $ZipFilename ` + -Username $Username ` + -Password $Password +} + +Export-ModuleMember -Function Initialize-ApiClient, Get-AuthHeader, Invoke-EnoceanGet, Send-MultipartFile, Send-EnoceanConfig diff --git a/modules/XmlParser.psm1 b/modules/XmlParser.psm1 index 6841279..d71f571 100644 --- a/modules/XmlParser.psm1 +++ b/modules/XmlParser.psm1 @@ -97,4 +97,34 @@ function Update-EnoceanDeviceId { } } -Export-ModuleMember -Function Get-DeviceListFromJson, Parse-EnoceanDeviceXml, New-EnoceanDeviceXml, Update-EnoceanDeviceId +function Update-GfxDeviceIds { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$MainXmlContent, + + [Parameter(Mandatory)] + [hashtable]$DeviceIdMap # @{ "ancien_id" = "nouveau_id" } + ) + + $modifiedXml = $MainXmlContent + $replaceCount = 0 + + foreach ($oldId in $DeviceIdMap.Keys) { + $newId = $DeviceIdMap[$oldId] + $pattern = "$oldId" + $replacement = "$newId" + + if ($modifiedXml -match [regex]::Escape($pattern)) { + $modifiedXml = $modifiedXml -replace [regex]::Escape($pattern), $replacement + $replaceCount++ + } + } + + return @{ + ModifiedXml = $modifiedXml + ReplaceCount = $replaceCount + } +} + +Export-ModuleMember -Function Get-DeviceListFromJson, Parse-EnoceanDeviceXml, New-EnoceanDeviceXml, Update-EnoceanDeviceId, Update-GfxDeviceIds diff --git a/modules/ZipBuilder.psm1 b/modules/ZipBuilder.psm1 index 48275e9..fe0a228 100644 --- a/modules/ZipBuilder.psm1 +++ b/modules/ZipBuilder.psm1 @@ -52,4 +52,74 @@ function Get-ZipFilename { return "fullConfig.$hashString.zip" } -Export-ModuleMember -Function New-EnoceanZip, Get-ZipFilename +function Read-GfxMainXml { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [byte[]]$GfxBytes + ) + + $memStream = New-Object System.IO.MemoryStream(, $GfxBytes) + $archive = New-Object System.IO.Compression.ZipArchive($memStream, [System.IO.Compression.ZipArchiveMode]::Read) + + $entry = $archive.GetEntry("Main.xml") + if (-not $entry) { + $archive.Dispose() + $memStream.Close() + throw "Main.xml non trouve dans le fichier GFx" + } + + $entryStream = $entry.Open() + $reader = New-Object System.IO.StreamReader($entryStream, [System.Text.Encoding]::UTF8) + $content = $reader.ReadToEnd() + $reader.Close() + $entryStream.Close() + $archive.Dispose() + $memStream.Close() + + return $content +} + +function Update-GfxZip { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [byte[]]$GfxBytes, + + [Parameter(Mandatory)] + [string]$ModifiedMainXml + ) + + # Copier les bytes dans un MemoryStream modifiable + $memStream = New-Object System.IO.MemoryStream + $memStream.Write($GfxBytes, 0, $GfxBytes.Length) + $memStream.Position = 0 + + $archive = New-Object System.IO.Compression.ZipArchive($memStream, [System.IO.Compression.ZipArchiveMode]::Update, $true) + + # Supprimer l'ancien Main.xml et recreer avec le contenu modifie + $entry = $archive.GetEntry("Main.xml") + if ($entry) { + $entry.Delete() + } + + $newEntry = $archive.CreateEntry("Main.xml", [System.IO.Compression.CompressionLevel]::Optimal) + $entryStream = $newEntry.Open() + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + $writer = New-Object System.IO.StreamWriter($entryStream, $utf8NoBom) + $writer.Write($ModifiedMainXml) + $writer.Flush() + $writer.Close() + $entryStream.Close() + + $archive.Dispose() + + $newBytes = $memStream.ToArray() + $memStream.Close() + + Write-Log -Message "GFx mis a jour en memoire : $($newBytes.Length) octets" -Level INFO + + return $newBytes +} + +Export-ModuleMember -Function New-EnoceanZip, Get-ZipFilename, Read-GfxMainXml, Update-GfxZip