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