Implémentation CLI PowerShell Enocean Eclypse
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/plan
|
||||||
|
/.idea
|
||||||
214
EnoceanCLI.ps1
Normal file
214
EnoceanCLI.ps1
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# EnoceanCLI.ps1 - CLI Enocean pour automates Distech Controls Eclypse
|
||||||
|
# Lecture/ecriture des configurations Enocean via REST API
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Lire les colonnes dynamiques DeviceId_N / DeviceType_N
|
||||||
|
$xmlFiles = @()
|
||||||
|
$resourceIndex = 1
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$deviceIdCol = "DeviceId_$resourceIndex"
|
||||||
|
$deviceTypeCol = "DeviceType_$resourceIndex"
|
||||||
|
|
||||||
|
# Verifier si les colonnes existent
|
||||||
|
$props = $automate.PSObject.Properties.Name
|
||||||
|
if ($deviceIdCol -notin $props -or $deviceTypeCol -notin $props) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$deviceId = $automate.$deviceIdCol
|
||||||
|
$deviceType = $automate.$deviceTypeCol
|
||||||
|
|
||||||
|
# Ignorer si vide
|
||||||
|
if (-not $deviceId -or $deviceId -eq "" -or -not $deviceType -or $deviceType -eq "") {
|
||||||
|
$resourceIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$xmlContent = New-EnoceanDeviceXml `
|
||||||
|
-ResourceNumber $resourceIndex `
|
||||||
|
-DeviceId $deviceId `
|
||||||
|
-DeviceType $deviceType
|
||||||
|
|
||||||
|
$xmlFiles += @{
|
||||||
|
Name = "enoceandevice$resourceIndex.xml"
|
||||||
|
Content = $xmlContent
|
||||||
|
}
|
||||||
|
|
||||||
|
Update-Stats -Counter DevicesProcessed
|
||||||
|
Write-Log -Message "[$hostname] XML genere : device $resourceIndex (Id=$deviceId, Type=$deviceType)" -Level INFO
|
||||||
|
|
||||||
|
$resourceIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($xmlFiles.Count -eq 0) {
|
||||||
|
Write-Log -Message "[$hostname] Aucun device a ecrire - 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) ($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
|
||||||
146
modules/ApiClient.psm1
Normal file
146
modules/ApiClient.psm1
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Module ApiClient - Communication REST API Eclypse
|
||||||
|
|
||||||
|
function Initialize-ApiClient {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
# Forcer TLS 1.2
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
|
||||||
|
# Desactiver la verification SSL (certificats auto-signes)
|
||||||
|
if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) {
|
||||||
|
Add-Type @"
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
public class TrustAllCertsPolicy : ICertificatePolicy {
|
||||||
|
public bool CheckValidationResult(
|
||||||
|
ServicePoint srvPoint, X509Certificate certificate,
|
||||||
|
WebRequest request, int certificateProblem) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
|
||||||
|
|
||||||
|
Write-Log -Message "ApiClient initialise (TLS 1.2, SSL bypass)" -Level INFO
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-AuthHeader {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Username,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Password
|
||||||
|
)
|
||||||
|
|
||||||
|
$pair = "${Username}:${Password}"
|
||||||
|
$bytes = [System.Text.Encoding]::ASCII.GetBytes($pair)
|
||||||
|
$base64 = [System.Convert]::ToBase64String($bytes)
|
||||||
|
|
||||||
|
return @{
|
||||||
|
"Authorization" = "Basic $base64"
|
||||||
|
"Accept" = "application/json, text/json, text/x-json, text/javascript, application/xml, text/xml, text/plain"
|
||||||
|
"User-Agent" = "EC-gfxProgram/7.9.26006.1 (DC_API:v1; Win32NT 6.2)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-EnoceanGet {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$BaseUrl,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ApiBasePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ResourcePath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Username,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Password
|
||||||
|
)
|
||||||
|
|
||||||
|
$headers = Get-AuthHeader -Username $Username -Password $Password
|
||||||
|
$url = "$BaseUrl$ApiBasePath/$($ResourcePath.TrimStart('/'))"
|
||||||
|
|
||||||
|
Write-Log -Message "GET $url" -Level INFO
|
||||||
|
|
||||||
|
$response = Invoke-WebRequest -Uri $url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 30
|
||||||
|
|
||||||
|
# Si la reponse est un byte[] (encode=bin), decoder en string UTF-8
|
||||||
|
if ($response.Content -is [byte[]]) {
|
||||||
|
return [System.Text.Encoding]::UTF8.GetString($response.Content)
|
||||||
|
}
|
||||||
|
return $response.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
$headers = Get-AuthHeader -Username $Username -Password $Password
|
||||||
|
$url = "$BaseUrl$ApiBasePath/files/bacnet/inputConfiguration"
|
||||||
|
|
||||||
|
Write-Log -Message "POST $url (fichier: $ZipFilename, taille: $($ZipBytes.Length) octets)" -Level INFO
|
||||||
|
|
||||||
|
# Construction multipart manuelle (compatible PS 5.1, pas de -Form)
|
||||||
|
$boundary = [System.Guid]::NewGuid().ToString("N")
|
||||||
|
$headers["Content-Type"] = "multipart/form-data; boundary=$boundary"
|
||||||
|
|
||||||
|
$encoding = [System.Text.Encoding]::ASCII
|
||||||
|
|
||||||
|
# Partie avant le fichier
|
||||||
|
$headerPart = @"
|
||||||
|
--$boundary
|
||||||
|
Content-Disposition: form-data; name="File"; filename="$ZipFilename"
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
"@
|
||||||
|
# Partie finale
|
||||||
|
$footerPart = "`r`n--$boundary--`r`n"
|
||||||
|
|
||||||
|
$headerBytes = $encoding.GetBytes($headerPart.Replace("`n", "`r`n"))
|
||||||
|
$footerBytes = $encoding.GetBytes($footerPart)
|
||||||
|
|
||||||
|
# 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($footerBytes, 0, $footerBytes.Length)
|
||||||
|
$bodyBytes = $bodyStream.ToArray()
|
||||||
|
$bodyStream.Close()
|
||||||
|
|
||||||
|
$response = Invoke-WebRequest -Uri $url -Method POST -Headers $headers -Body $bodyBytes -UseBasicParsing -TimeoutSec 60
|
||||||
|
|
||||||
|
Write-Log -Message "POST reponse : $($response.StatusCode)" -Level SUCCESS
|
||||||
|
return $response
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Initialize-ApiClient, Get-AuthHeader, Invoke-EnoceanGet, Send-EnoceanConfig
|
||||||
155
modules/CsvHandler.psm1
Normal file
155
modules/CsvHandler.psm1
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Module CsvHandler - Lecture/ecriture CSV automates
|
||||||
|
|
||||||
|
function Read-AutomateCsv {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$CsvPath
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path $CsvPath)) {
|
||||||
|
throw "Fichier CSV introuvable : $CsvPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = Import-Csv -Path $CsvPath -Delimiter ";"
|
||||||
|
Write-Log -Message "CSV charge : $($rows.Count) lignes depuis $CsvPath" -Level INFO
|
||||||
|
return $rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-BaseUrl {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[PSCustomObject]$Automate
|
||||||
|
)
|
||||||
|
|
||||||
|
$ip = $Automate."Current Ip"
|
||||||
|
$httpsPort = $Automate.HttpsPort
|
||||||
|
$httpPort = $Automate.HttpPort
|
||||||
|
|
||||||
|
# HTTPS si port > 0 et != -1
|
||||||
|
if ($httpsPort -and $httpsPort -ne "" -and [int]$httpsPort -gt 0 -and [int]$httpsPort -ne -1) {
|
||||||
|
if ([int]$httpsPort -eq 443) {
|
||||||
|
return "https://$ip"
|
||||||
|
}
|
||||||
|
return "https://${ip}:$httpsPort"
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP si port > 0
|
||||||
|
if ($httpPort -and $httpPort -ne "" -and [int]$httpPort -gt 0 -and [int]$httpPort -ne -1) {
|
||||||
|
if ([int]$httpPort -eq 80) {
|
||||||
|
return "http://$ip"
|
||||||
|
}
|
||||||
|
return "http://${ip}:$httpPort"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ApiBasePath {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[PSCustomObject]$Automate
|
||||||
|
)
|
||||||
|
|
||||||
|
$restUrl = $Automate.RestServiceURL
|
||||||
|
if ($restUrl -and $restUrl -ne "") {
|
||||||
|
return $restUrl.TrimEnd("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback v1
|
||||||
|
return "/api/rest/v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Credentials {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[PSCustomObject]$Automate,
|
||||||
|
|
||||||
|
[string]$DefaultUsername = "admin",
|
||||||
|
[string]$DefaultPassword = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$username = $DefaultUsername
|
||||||
|
$password = $DefaultPassword
|
||||||
|
|
||||||
|
# Override depuis le CSV si renseigne
|
||||||
|
if ($Automate.Username -and $Automate.Username -ne "") {
|
||||||
|
$username = $Automate.Username
|
||||||
|
}
|
||||||
|
if ($Automate.Password -and $Automate.Password -ne "") {
|
||||||
|
$password = $Automate.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
return @{
|
||||||
|
Username = $username
|
||||||
|
Password = $password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-OutputCsv {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[array]$InputRows,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable]$DeviceData,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$OutputPath
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trouver le nombre max de devices parmi tous les automates
|
||||||
|
$maxDevices = 0
|
||||||
|
foreach ($key in $DeviceData.Keys) {
|
||||||
|
$count = $DeviceData[$key].Count
|
||||||
|
if ($count -gt $maxDevices) {
|
||||||
|
$maxDevices = $count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Message "Max devices par automate : $maxDevices" -Level INFO
|
||||||
|
|
||||||
|
# Construire les lignes de sortie
|
||||||
|
$outputRows = @()
|
||||||
|
foreach ($row in $InputRows) {
|
||||||
|
# Copier toutes les proprietes existantes
|
||||||
|
$obj = [ordered]@{}
|
||||||
|
foreach ($prop in $row.PSObject.Properties) {
|
||||||
|
$obj[$prop.Name] = $prop.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajouter les colonnes dynamiques DeviceId_N / DeviceType_N
|
||||||
|
$ip = $row."Current Ip"
|
||||||
|
$devices = @()
|
||||||
|
if ($DeviceData.ContainsKey($ip)) {
|
||||||
|
$devices = $DeviceData[$ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 1; $i -le $maxDevices; $i++) {
|
||||||
|
if ($i -le $devices.Count) {
|
||||||
|
$obj["DeviceId_$i"] = $devices[$i - 1].DeviceId
|
||||||
|
$obj["DeviceType_$i"] = $devices[$i - 1].DeviceType
|
||||||
|
} else {
|
||||||
|
$obj["DeviceId_$i"] = ""
|
||||||
|
$obj["DeviceType_$i"] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputRows += [PSCustomObject]$obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Creer le repertoire de sortie si necessaire
|
||||||
|
$outputDir = Split-Path $OutputPath -Parent
|
||||||
|
if ($outputDir -and -not (Test-Path $outputDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputRows | Export-Csv -Path $OutputPath -Delimiter ";" -NoTypeInformation -Encoding UTF8
|
||||||
|
Write-Log -Message "CSV de sortie ecrit : $OutputPath ($($outputRows.Count) lignes)" -Level SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Read-AutomateCsv, Get-BaseUrl, Get-ApiBasePath, Get-Credentials, Write-OutputCsv
|
||||||
95
modules/Logger.psm1
Normal file
95
modules/Logger.psm1
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Module Logger - Gestion des logs console + fichier
|
||||||
|
|
||||||
|
$script:LogFile = $null
|
||||||
|
$script:Stopwatch = $null
|
||||||
|
$script:Stats = @{
|
||||||
|
AutomatesTotal = 0
|
||||||
|
AutomatesError = 0
|
||||||
|
DevicesProcessed = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initialize-Logger {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format "yyyy-MM-dd_HH\hmm"
|
||||||
|
$script:LogFile = Join-Path (Get-Location) "enocean_$timestamp.log"
|
||||||
|
New-Item -ItemType File -Path $script:LogFile -Force | Out-Null
|
||||||
|
|
||||||
|
$script:Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
|
||||||
|
$script:Stats = @{
|
||||||
|
AutomatesTotal = 0
|
||||||
|
AutomatesError = 0
|
||||||
|
DevicesProcessed = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Message "Logger initialise - fichier: $($script:LogFile)" -Level INFO
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Log {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$Message,
|
||||||
|
|
||||||
|
[ValidateSet("INFO", "WARN", "ERROR", "SUCCESS")]
|
||||||
|
[string]$Level = "INFO"
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||||
|
$line = "[$timestamp] [$Level] $Message"
|
||||||
|
|
||||||
|
# Couleur console selon le niveau
|
||||||
|
switch ($Level) {
|
||||||
|
"INFO" { Write-Host $line -ForegroundColor Cyan }
|
||||||
|
"WARN" { Write-Host $line -ForegroundColor Yellow }
|
||||||
|
"ERROR" { Write-Host $line -ForegroundColor Red }
|
||||||
|
"SUCCESS" { Write-Host $line -ForegroundColor Green }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ecriture fichier
|
||||||
|
if ($script:LogFile) {
|
||||||
|
Add-Content -Path $script:LogFile -Value $line -Encoding UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-Stats {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidateSet("AutomatesTotal", "AutomatesError", "DevicesProcessed")]
|
||||||
|
[string]$Counter,
|
||||||
|
|
||||||
|
[int]$Increment = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
$script:Stats[$Counter] += $Increment
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Summary {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
$script:Stopwatch.Stop()
|
||||||
|
$duration = $script:Stopwatch.Elapsed
|
||||||
|
|
||||||
|
Write-Log -Message "========== RESUME ==========" -Level INFO
|
||||||
|
Write-Log -Message "Automates traites : $($script:Stats.AutomatesTotal)" -Level INFO
|
||||||
|
Write-Log -Message "Automates en erreur : $($script:Stats.AutomatesError)" -Level $(if ($script:Stats.AutomatesError -gt 0) { "WARN" } else { "INFO" })
|
||||||
|
Write-Log -Message "Devices traites : $($script:Stats.DevicesProcessed)" -Level INFO
|
||||||
|
Write-Log -Message "Duree totale : $($duration.ToString('hh\:mm\:ss\.ff'))" -Level INFO
|
||||||
|
Write-Log -Message "============================" -Level INFO
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-LogDirectory {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param()
|
||||||
|
|
||||||
|
if ($script:LogFile) {
|
||||||
|
return Split-Path $script:LogFile -Parent
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Initialize-Logger, Write-Log, Update-Stats, Write-Summary, Get-LogDirectory
|
||||||
74
modules/XmlParser.psm1
Normal file
74
modules/XmlParser.psm1
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Module XmlParser - Parse et generation XML Enocean
|
||||||
|
|
||||||
|
function Get-DeviceListFromJson {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$JsonContent
|
||||||
|
)
|
||||||
|
|
||||||
|
$json = $JsonContent | ConvertFrom-Json
|
||||||
|
|
||||||
|
$devices = @()
|
||||||
|
if ($json.files) {
|
||||||
|
foreach ($file in $json.files) {
|
||||||
|
$devices += @{
|
||||||
|
Name = $file.path.name
|
||||||
|
Href = $file.path.href
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return , $devices
|
||||||
|
}
|
||||||
|
|
||||||
|
function Parse-EnoceanDeviceXml {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$XmlContent
|
||||||
|
)
|
||||||
|
|
||||||
|
[xml]$xml = $XmlContent
|
||||||
|
$config = $xml.EnOceanDevice.Configuration
|
||||||
|
|
||||||
|
return @{
|
||||||
|
ResourceNumber = [int]$config.ResourceNumber
|
||||||
|
DeviceId = $config.DeviceId
|
||||||
|
DeviceType = $config.DeviceType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-EnoceanDeviceXml {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[int]$ResourceNumber,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$DeviceId,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$DeviceType
|
||||||
|
)
|
||||||
|
|
||||||
|
$xml = @"
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<EnOceanDevice>
|
||||||
|
<Configuration>
|
||||||
|
<ResourceType>EnOceanDevice</ResourceType>
|
||||||
|
<ResourceNumber>$ResourceNumber</ResourceNumber>
|
||||||
|
<Name>EnOcean Device $ResourceNumber</Name>
|
||||||
|
<DeviceId>$DeviceId</DeviceId>
|
||||||
|
<DeviceType>$DeviceType</DeviceType>
|
||||||
|
<MaxReceiveTime>2400</MaxReceiveTime>
|
||||||
|
<Points></Points>
|
||||||
|
<Description></Description>
|
||||||
|
</Configuration>
|
||||||
|
</EnOceanDevice>
|
||||||
|
"@
|
||||||
|
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Get-DeviceListFromJson, Parse-EnoceanDeviceXml, New-EnoceanDeviceXml
|
||||||
54
modules/ZipBuilder.psm1
Normal file
54
modules/ZipBuilder.psm1
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Module ZipBuilder - Creation ZIP en memoire pour envoi config Enocean
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.IO.Compression
|
||||||
|
|
||||||
|
function New-EnoceanZip {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[hashtable[]]$XmlFiles # Chaque hashtable : @{ Name = "enoceandevice1.xml"; Content = "<xml>..." }
|
||||||
|
)
|
||||||
|
|
||||||
|
$memoryStream = New-Object System.IO.MemoryStream
|
||||||
|
|
||||||
|
# leaveOpen = $true pour pouvoir lire le stream apres fermeture de l'archive
|
||||||
|
$archive = New-Object System.IO.Compression.ZipArchive($memoryStream, [System.IO.Compression.ZipArchiveMode]::Create, $true)
|
||||||
|
|
||||||
|
foreach ($xmlFile in $XmlFiles) {
|
||||||
|
# Chemin interne avec forward slashes
|
||||||
|
$entryPath = "enocean/configuration/devices/$($xmlFile.Name)"
|
||||||
|
$entry = $archive.CreateEntry($entryPath, [System.IO.Compression.CompressionLevel]::Optimal)
|
||||||
|
|
||||||
|
$entryStream = $entry.Open()
|
||||||
|
$writer = New-Object System.IO.StreamWriter($entryStream, [System.Text.Encoding]::UTF8)
|
||||||
|
$writer.Write($xmlFile.Content)
|
||||||
|
$writer.Flush()
|
||||||
|
$writer.Close()
|
||||||
|
$entryStream.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
$archive.Dispose()
|
||||||
|
|
||||||
|
$zipBytes = $memoryStream.ToArray()
|
||||||
|
$memoryStream.Close()
|
||||||
|
|
||||||
|
Write-Log -Message "ZIP cree en memoire : $($XmlFiles.Count) fichier(s), $($zipBytes.Length) octets" -Level INFO
|
||||||
|
|
||||||
|
return $zipBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ZipFilename {
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[byte[]]$ZipBytes
|
||||||
|
)
|
||||||
|
|
||||||
|
$md5 = [System.Security.Cryptography.MD5]::Create()
|
||||||
|
$hashBytes = $md5.ComputeHash($ZipBytes)
|
||||||
|
$hashString = ($hashBytes | ForEach-Object { $_.ToString("x2") }) -join ""
|
||||||
|
|
||||||
|
return "fullConfig.$hashString.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function New-EnoceanZip, Get-ZipFilename
|
||||||
Reference in New Issue
Block a user