359 lines
14 KiB
PowerShell
359 lines
14 KiB
PowerShell
<#
|
|
.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
|