Files
gfx-enocean/EnoceanCLI.ps1
Charles 83df3cc4ef README.md — Documentation complète du projet
Refonte action Write : modifier les XML existants au lieu de les regenerer
2026-03-04 08:16:23 +01:00

318 lines
12 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 = @()
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
}
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 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 envoyee avec succes" -Level SUCCESS
}
catch {
Write-Log -Message "[$hostname] ERREUR : $($_.Exception.Message)" -Level ERROR
Update-Stats -Counter AutomatesError
}
}
}
# Resume final
Write-Summary