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