Implémentation CLI PowerShell Enocean Eclypse

This commit is contained in:
2026-03-03 23:26:20 +01:00
commit c75c731ddc
7 changed files with 740 additions and 0 deletions

146
modules/ApiClient.psm1 Normal file
View 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
View 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
View 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
View 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
View 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