<# .SYNOPSIS CLI Enocean pour automates Distech Controls Eclypse. Lecture et ecriture des configurations Enocean via REST API. .DESCRIPTION EnoceanCLI permet de gerer les configurations EnOcean sur des automates Distech Controls Eclypse Gen 1 (ECY-xxx) en utilisant leur API REST. Action READ : Lit la configuration EnOcean de chaque automate et genere un fichier CSV avec les DeviceId et DeviceType de chaque device. Action WRITE : Modifie les DeviceId sur chaque automate a partir du CSV. Seul le DeviceId est modifie, toute la configuration existante (Points, MaxReceiveTime, Description...) est preservee. .PARAMETER Action Action a effectuer : Read - Lire la configuration et generer un CSV de sortie Write - Ecrire les DeviceId depuis le CSV vers les automates .PARAMETER CsvInput Chemin vers le fichier CSV d'entree (separateur point-virgule). Le CSV doit contenir au minimum : Hostname, Current Ip, HttpPort, HttpsPort. Pour l'action Write, il doit aussi contenir les colonnes DeviceId_1, DeviceId_2, etc. .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 .\EnoceanCLI.ps1 -Action Read -CsvInput ".\automates.csv" Lit la configuration EnOcean de tous les automates listes dans le CSV. Genere un fichier enocean_YYYY-MM-DD_HHhMM.csv dans le repertoire courant. .EXAMPLE .\EnoceanCLI.ps1 -Action Write -CsvInput ".\enocean_2026-03-03_23h07.csv" -Password "MonMotDePasse" Ecrit les DeviceId du CSV vers les automates. Les configurations existantes (Points, MaxReceiveTime, etc.) sont preservees. .EXAMPLE Get-Help .\EnoceanCLI.ps1 -Detailed Affiche cette aide detaillee. .NOTES Prerequis : PowerShell 5.1+ (inclus dans Windows 10/11) API : Distech Controls Eclypse REST API v1 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 "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 # GET liste des devices Enocean existants sur l'automate $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 existant sur l'automate - ignore" -Level WARN continue } Write-Log -Message "[$hostname] $($deviceFiles.Count) device(s) existant(s) sur l'automate" -Level INFO # GET chaque XML existant et parser pour obtenir ResourceNumber $existingDevices = @() 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 $existingDevices += @{ Name = $deviceFile.Name XmlContent = $xmlContent ResourceNumber = $parsed.ResourceNumber DeviceType = $parsed.DeviceType DeviceId = $parsed.DeviceId } } catch { Write-Log -Message "[$hostname] Erreur lecture $($deviceFile.Name) : $($_.Exception.Message)" -Level ERROR } } # Trier par ResourceNumber $existingDevices = @($existingDevices | Sort-Object { $_.ResourceNumber }) # 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" $deviceTypeCol = "DeviceType_$seqIndex" $device = $existingDevices[$i] # Verifier si la colonne DeviceId_N existe dans le CSV $props = $automate.PSObject.Properties.Name if ($deviceIdCol -notin $props) { Write-Log -Message "[$hostname] Colonne $deviceIdCol absente du CSV - device $($device.Name) non modifie" -Level WARN continue } $newDeviceId = $automate.$deviceIdCol # Ignorer si DeviceId vide if (-not $newDeviceId -or $newDeviceId -eq "") { $seqIndex++ continue } # Verifier DeviceType CSV vs XML si la colonne existe if ($deviceTypeCol -in $props) { $csvDeviceType = $automate.$deviceTypeCol if ($csvDeviceType -and $csvDeviceType -ne "" -and $csvDeviceType -ne $device.DeviceType) { Write-Log -Message "[$hostname] Device $($device.Name) : DeviceType CSV ($csvDeviceType) differe du XML ($($device.DeviceType))" -Level WARN } } # Modifier uniquement le DeviceId dans le XML existant $result = Update-EnoceanDeviceId -XmlContent $device.XmlContent -NewDeviceId $newDeviceId $xmlFiles += @{ Name = $device.Name 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 } if ($xmlFiles.Count -eq 0) { Write-Log -Message "[$hostname] Aucun device a modifier - ignore" -Level WARN continue } # Creer le ZIP enocean et l'envoyer $zipBytes = New-EnoceanZip -XmlFiles $xmlFiles $zipFilename = Get-ZipFilename -ZipBytes $zipBytes Write-Log -Message "[$hostname] Envoi de $($xmlFiles.Count) device(s) modifie(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 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 Update-Stats -Counter AutomatesError } } } # Resume final Write-Summary