Ajout initial
Fichiers : - EditOffsetCLI.ps1 — script PowerShell principal (modification BLE + GFx) - README.md — documentation utilisateur en français - .gitignore — configuration git
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
*.log
|
||||
*.csv
|
||||
/.idea
|
||||
/doc
|
||||
/plan
|
||||
/.claude
|
||||
@@ -0,0 +1,752 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
EditOffsetCLI - Modification en masse de la consigne relative UNITOUCH sur automates Distech Eclypse.
|
||||
|
||||
.DESCRIPTION
|
||||
Modifie les high-limit et low-limit des local-values de consigne, ainsi que la
|
||||
relative-value de toutes les configurations BLE, sur un parc d'automates Distech
|
||||
Controls Eclypse via leur API REST v2.
|
||||
|
||||
Optionnellement, modifie aussi le fichier GFx de l'automate pour synchroniser
|
||||
les limites de consigne dans l'interface graphique (parametre -UpdateGfx).
|
||||
|
||||
Pour chaque automate du CSV :
|
||||
1. GET /ble-room-device/ pour identifier les local-values de consigne et les configurations
|
||||
2. Pour chaque local-value : GET -> modifier high-limit/low-limit -> POST
|
||||
3. Pour chaque configuration : GET -> modifier relative-value -> POST
|
||||
4. Si -UpdateGfx : telecharger le GFx -> modifier Main.xml -> uploader
|
||||
|
||||
.PARAMETER CsvInput
|
||||
Chemin vers le fichier CSV d'entree (separateur point-virgule).
|
||||
Colonnes obligatoires : Hostname, Current Ip, HttpPort, HttpsPort
|
||||
Colonnes optionnelles : Username, Password (surchargent les parametres CLI)
|
||||
|
||||
.PARAMETER Offset
|
||||
Valeur de l'offset de consigne.
|
||||
Resultat : high-limit = +Offset, low-limit = -Offset, relative-value = Offset
|
||||
|
||||
.PARAMETER Username
|
||||
Nom d'utilisateur pour l'authentification API (defaut: admin).
|
||||
|
||||
.PARAMETER Password
|
||||
Mot de passe pour l'authentification API (defaut: vide).
|
||||
|
||||
.PARAMETER UpdateGfx
|
||||
Switch optionnel. Si present, modifie aussi le fichier GFx (project/Project.gfx)
|
||||
de chaque automate pour synchroniser les limites dans l'interface graphique.
|
||||
Sans ce parametre, seule la configuration BLE est modifiee.
|
||||
|
||||
.EXAMPLE
|
||||
.\EditOffsetCLI.ps1 -CsvInput ".\automates.csv" -Offset 3
|
||||
|
||||
Modifie uniquement la configuration BLE (local-values + configurations).
|
||||
|
||||
.EXAMPLE
|
||||
.\EditOffsetCLI.ps1 -CsvInput ".\automates.csv" -Offset 3 -UpdateGfx
|
||||
|
||||
Modifie la configuration BLE ET le fichier GFx de chaque automate.
|
||||
|
||||
.EXAMPLE
|
||||
.\EditOffsetCLI.ps1 -CsvInput ".\automates.csv" -Offset 3 -Username "admin" -Password "secret" -UpdateGfx
|
||||
|
||||
.NOTES
|
||||
Prerequis : PowerShell 5.1+ (inclus dans Windows 10/11)
|
||||
API : Distech Controls Eclypse REST API v2
|
||||
Securite : TLS 1.2, certificats auto-signes acceptes, fallback curl.exe
|
||||
Fichier unique pour faciliter le transfert (un seul Unblock-File necessaire).
|
||||
|
||||
Si Windows bloque le fichier apres telechargement :
|
||||
Unblock-File .\EditOffsetCLI.ps1
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$CsvInput,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[double]$Offset,
|
||||
|
||||
[string]$Username = "admin",
|
||||
|
||||
[string]$Password = "",
|
||||
|
||||
[switch]$UpdateGfx
|
||||
)
|
||||
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# ============================================================
|
||||
#region LOGGER
|
||||
# ============================================================
|
||||
|
||||
$script:LogFile = $null
|
||||
$script:Stopwatch = $null
|
||||
$script:Stats = @{}
|
||||
|
||||
function Initialize-Logger {
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd_HH\hmm"
|
||||
$script:LogFile = Join-Path (Get-Location) "editoffset_$timestamp.log"
|
||||
New-Item -ItemType File -Path $script:LogFile -Force | Out-Null
|
||||
$script:Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$script:Stats = @{
|
||||
AutomatesTotal = 0
|
||||
AutomatesError = 0
|
||||
LocalValuesUpdated = 0
|
||||
ConfigsUpdated = 0
|
||||
GfxUpdated = 0
|
||||
}
|
||||
Write-Log -Message "Logger initialise - fichier: $($script:LogFile)" -Level INFO
|
||||
}
|
||||
|
||||
function Write-Log {
|
||||
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"
|
||||
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 }
|
||||
}
|
||||
if ($script:LogFile) {
|
||||
Add-Content -Path $script:LogFile -Value $line -Encoding UTF8
|
||||
}
|
||||
}
|
||||
|
||||
function Update-Stats {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("AutomatesTotal", "AutomatesError", "LocalValuesUpdated", "ConfigsUpdated", "GfxUpdated")]
|
||||
[string]$Counter,
|
||||
[int]$Increment = 1
|
||||
)
|
||||
$script:Stats[$Counter] += $Increment
|
||||
}
|
||||
|
||||
function Write-Summary {
|
||||
$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 "Local-values mises a jour : $($script:Stats.LocalValuesUpdated)" -Level INFO
|
||||
Write-Log -Message "Configurations mises a jour : $($script:Stats.ConfigsUpdated)" -Level INFO
|
||||
Write-Log -Message "GFx mis a jour : $($script:Stats.GfxUpdated)" -Level INFO
|
||||
Write-Log -Message "Duree totale : $($duration.ToString('hh\:mm\:ss\.ff'))" -Level INFO
|
||||
Write-Log -Message "============================" -Level INFO
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
# ============================================================
|
||||
#region API CLIENT
|
||||
# ============================================================
|
||||
|
||||
$script:CurlAvailable = $false
|
||||
|
||||
function Initialize-ApiClient {
|
||||
[Net.ServicePointManager]::SecurityProtocol = `
|
||||
[Net.SecurityProtocolType]::Tls12 -bor `
|
||||
[Net.SecurityProtocolType]::Tls11 -bor `
|
||||
[Net.SecurityProtocolType]::Tls
|
||||
|
||||
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
|
||||
|
||||
$script:CurlAvailable = [bool](Get-Command curl.exe -ErrorAction SilentlyContinue)
|
||||
$extra = if ($script:CurlAvailable) { ", curl.exe fallback" } else { "" }
|
||||
Write-Log -Message "ApiClient initialise (TLS multi, SSL bypass$extra)" -Level INFO
|
||||
}
|
||||
|
||||
function Get-AuthHeader {
|
||||
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"
|
||||
"User-Agent" = "EditOffsetCLI/1.0 (PowerShell; Win32NT)"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-SslError {
|
||||
param([Parameter(Mandatory)][System.Exception]$Exception)
|
||||
return ($Exception.Message -match "SSL|TLS|confiance|trust|certificate")
|
||||
}
|
||||
|
||||
function Invoke-ApiGet {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Url,
|
||||
[Parameter(Mandatory)][string]$Username,
|
||||
[Parameter(Mandatory)][AllowEmptyString()][string]$Password
|
||||
)
|
||||
$headers = Get-AuthHeader -Username $Username -Password $Password
|
||||
Write-Log -Message "GET $Url" -Level INFO
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 30
|
||||
if ($response.Content -is [byte[]]) {
|
||||
return [System.Text.Encoding]::UTF8.GetString($response.Content).TrimStart([char]0xFEFF)
|
||||
}
|
||||
return $response.Content
|
||||
}
|
||||
catch {
|
||||
if ((Test-SslError -Exception $_.Exception) -and $script:CurlAvailable) {
|
||||
Write-Log -Message "GET $Url (curl -k fallback)" -Level WARN
|
||||
$output = & curl.exe -s -k -u "${Username}:${Password}" -H "Accept: application/json" $Url 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "curl GET erreur (exit $LASTEXITCODE): $output" }
|
||||
Write-Log -Message "GET OK (curl)" -Level SUCCESS
|
||||
return ($output | Out-String)
|
||||
}
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ApiPost {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Url,
|
||||
[Parameter(Mandatory)][string]$JsonBody,
|
||||
[Parameter(Mandatory)][string]$Username,
|
||||
[Parameter(Mandatory)][AllowEmptyString()][string]$Password
|
||||
)
|
||||
$headers = Get-AuthHeader -Username $Username -Password $Password
|
||||
Write-Log -Message "POST $Url" -Level INFO
|
||||
try {
|
||||
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($JsonBody)
|
||||
$response = Invoke-WebRequest -Uri $Url -Method POST -Headers $headers -Body $bodyBytes `
|
||||
-ContentType "application/json; charset=utf-8" -UseBasicParsing -TimeoutSec 30
|
||||
Write-Log -Message "POST reponse : $($response.StatusCode)" -Level SUCCESS
|
||||
return $response.StatusCode
|
||||
}
|
||||
catch {
|
||||
if ((Test-SslError -Exception $_.Exception) -and $script:CurlAvailable) {
|
||||
Write-Log -Message "POST $Url (curl -k fallback)" -Level WARN
|
||||
$tempFile = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
[System.IO.File]::WriteAllText($tempFile, $JsonBody, [System.Text.Encoding]::UTF8)
|
||||
$output = & curl.exe -s -k -u "${Username}:${Password}" `
|
||||
-H "Content-Type: application/json; charset=utf-8" `
|
||||
-d "@$tempFile" $Url 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "curl POST erreur (exit $LASTEXITCODE): $output" }
|
||||
Write-Log -Message "POST OK (curl)" -Level SUCCESS
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
return
|
||||
}
|
||||
Write-Log -Message "POST erreur : $($_.Exception.Message)" -Level ERROR
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ApiGetBinary {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Url,
|
||||
[Parameter(Mandatory)][string]$Username,
|
||||
[Parameter(Mandatory)][AllowEmptyString()][string]$Password
|
||||
)
|
||||
$headers = Get-AuthHeader -Username $Username -Password $Password
|
||||
$headers["Accept"] = "*/*"
|
||||
Write-Log -Message "GET (binary) $Url" -Level INFO
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -Method GET -Headers $headers -UseBasicParsing -TimeoutSec 60
|
||||
if ($response.Content -is [byte[]]) {
|
||||
return $response.Content
|
||||
}
|
||||
# Fallback : contenu retourne comme string — encoder en latin-1 pour preserver les octets
|
||||
return [System.Text.Encoding]::GetEncoding('iso-8859-1').GetBytes($response.Content)
|
||||
}
|
||||
catch {
|
||||
if ((Test-SslError -Exception $_.Exception) -and $script:CurlAvailable) {
|
||||
Write-Log -Message "GET (binary) $Url (curl -k fallback)" -Level WARN
|
||||
$tempOut = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
& curl.exe -s -k -u "${Username}:${Password}" -o $tempOut $Url 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "curl GET binary erreur (exit $LASTEXITCODE)" }
|
||||
Write-Log -Message "GET binary OK (curl)" -Level SUCCESS
|
||||
return [System.IO.File]::ReadAllBytes($tempOut)
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tempOut -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ApiPostBinary {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Url,
|
||||
[Parameter(Mandatory)][byte[]]$Bytes,
|
||||
[Parameter(Mandatory)][string]$Username,
|
||||
[Parameter(Mandatory)][AllowEmptyString()][string]$Password
|
||||
)
|
||||
$headers = Get-AuthHeader -Username $Username -Password $Password
|
||||
$boundary = "----FormBoundary" + [System.Guid]::NewGuid().ToString("N")
|
||||
|
||||
# Corps multipart : header de part + bytes du fichier + footer
|
||||
$partHeader = [System.Text.Encoding]::ASCII.GetBytes(
|
||||
"--$boundary`r`n" +
|
||||
"Content-Disposition: form-data; name=`"file`"; filename=`"Project.gfx`"`r`n" +
|
||||
"Content-Type: application/octet-stream`r`n`r`n")
|
||||
$partFooter = [System.Text.Encoding]::ASCII.GetBytes("`r`n--$boundary--`r`n")
|
||||
|
||||
$body = New-Object byte[] ($partHeader.Length + $Bytes.Length + $partFooter.Length)
|
||||
[System.Array]::Copy($partHeader, 0, $body, 0, $partHeader.Length)
|
||||
[System.Array]::Copy($Bytes, 0, $body, $partHeader.Length, $Bytes.Length)
|
||||
[System.Array]::Copy($partFooter, 0, $body, $partHeader.Length + $Bytes.Length, $partFooter.Length)
|
||||
|
||||
Write-Log -Message "POST (multipart) $Url" -Level INFO
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -Method POST -Headers $headers -Body $body `
|
||||
-ContentType "multipart/form-data; boundary=$boundary" `
|
||||
-UseBasicParsing -TimeoutSec 60
|
||||
Write-Log -Message "POST multipart reponse : $($response.StatusCode)" -Level SUCCESS
|
||||
return $response.StatusCode
|
||||
}
|
||||
catch {
|
||||
if ((Test-SslError -Exception $_.Exception) -and $script:CurlAvailable) {
|
||||
Write-Log -Message "POST (multipart) $Url (curl -k fallback)" -Level WARN
|
||||
$tempFile = [System.IO.Path]::GetTempFileName() + ".gfx"
|
||||
try {
|
||||
[System.IO.File]::WriteAllBytes($tempFile, $Bytes)
|
||||
$output = & curl.exe -s -k -u "${Username}:${Password}" `
|
||||
-F "file=@`"$tempFile`";filename=Project.gfx" $Url 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "curl POST multipart erreur (exit $LASTEXITCODE): $output" }
|
||||
Write-Log -Message "POST multipart OK (curl)" -Level SUCCESS
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
return
|
||||
}
|
||||
Write-Log -Message "POST multipart erreur : $($_.Exception.Message)" -Level ERROR
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
# ============================================================
|
||||
#region CSV HANDLER
|
||||
# ============================================================
|
||||
|
||||
function Read-AutomateCsv {
|
||||
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) ligne(s) depuis $CsvPath" -Level INFO
|
||||
return $rows
|
||||
}
|
||||
|
||||
function Get-BaseUrl {
|
||||
param([Parameter(Mandatory)][PSCustomObject]$Automate)
|
||||
$ip = $Automate."Current Ip"
|
||||
$props = $Automate.PSObject.Properties.Name
|
||||
|
||||
# Utiliser HttpsPort si la colonne est presente et valide
|
||||
if ("HttpsPort" -in $props) {
|
||||
$httpsPort = $Automate.HttpsPort
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
# Utiliser HttpPort si la colonne est presente et valide
|
||||
if ("HttpPort" -in $props) {
|
||||
$httpPort = $Automate.HttpPort
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
# Defaut ECY Facilities V2 : toujours HTTPS port 443
|
||||
return "https://$ip"
|
||||
}
|
||||
|
||||
function Get-Credentials {
|
||||
param(
|
||||
[Parameter(Mandatory)][PSCustomObject]$Automate,
|
||||
[string]$DefaultUsername = "admin",
|
||||
[string]$DefaultPassword = ""
|
||||
)
|
||||
$u = $DefaultUsername
|
||||
$p = $DefaultPassword
|
||||
$props = $Automate.PSObject.Properties.Name
|
||||
if ("Username" -in $props -and $Automate.Username -and $Automate.Username -ne "") { $u = $Automate.Username }
|
||||
if ("Password" -in $props -and $Automate.Password -and $Automate.Password -ne "") { $p = $Automate.Password }
|
||||
return @{ Username = $u; Password = $p }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
# ============================================================
|
||||
#region UNITOUCH PARSER
|
||||
# ============================================================
|
||||
|
||||
function Get-SetpointLocalValueKeys {
|
||||
# Extrait les cles de local-values a modifier depuis le JSON du device.
|
||||
# Les cles sont lues dans configurations/{id}/setpoint-temperature/command-value
|
||||
# et feedback-value pour ne pas supposer que ce sont toujours 101/102.
|
||||
param([Parameter(Mandatory)][string]$JsonContent)
|
||||
|
||||
$device = $JsonContent | ConvertFrom-Json
|
||||
$keys = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
foreach ($configProp in $device.configurations.PSObject.Properties) {
|
||||
$sp = $configProp.Value."setpoint-temperature"
|
||||
if (-not $sp) { continue }
|
||||
$cmdKey = [string]$sp."command-value"
|
||||
$fbKey = [string]$sp."feedback-value"
|
||||
if ($cmdKey -and -not $keys.Contains($cmdKey)) { $keys.Add($cmdKey) }
|
||||
if ($fbKey -and -not $keys.Contains($fbKey)) { $keys.Add($fbKey) }
|
||||
}
|
||||
return $keys
|
||||
}
|
||||
|
||||
function Get-AllConfigIds {
|
||||
# Retourne toutes les cles de configurations presentes dans le device.
|
||||
param([Parameter(Mandatory)][string]$JsonContent)
|
||||
$device = $JsonContent | ConvertFrom-Json
|
||||
return $device.configurations.PSObject.Properties.Name
|
||||
}
|
||||
|
||||
function Test-HasRelativeSetpoint {
|
||||
# Verifie qu'au moins une configuration expose "relative-value" dans setpoint-temperature.
|
||||
# Si absent, le thermostat est en mode absolu et le script ne doit rien modifier.
|
||||
param([Parameter(Mandatory)][string]$JsonContent)
|
||||
$device = $JsonContent | ConvertFrom-Json
|
||||
foreach ($configProp in $device.configurations.PSObject.Properties) {
|
||||
$sp = $configProp.Value."setpoint-temperature"
|
||||
if ($sp -and ($sp.PSObject.Properties.Name -contains "relative-value")) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-GfxSetpointIdxList {
|
||||
# Lit les ValueIndex de consigne depuis les configurations GFx (IP2SmartSenseSimpleConfigurationResource).
|
||||
# Retourne la liste dedupliquee des IDX des SmartSenseNumericValue a modifier.
|
||||
# Approche dynamique : ne suppose pas que les IDX sont toujours 108/109.
|
||||
param([Parameter(Mandatory)][string]$XmlContent)
|
||||
|
||||
$idxList = [System.Collections.Generic.List[string]]::new()
|
||||
$opts = [System.Text.RegularExpressions.RegexOptions]::Singleline
|
||||
|
||||
$spMatches = [regex]::Matches($XmlContent,
|
||||
'<TemperatureSetpointValue\b[^>]*>.*?<ValueIndex>(\d+)</ValueIndex>', $opts)
|
||||
$fbMatches = [regex]::Matches($XmlContent,
|
||||
'<TemperatureSetpointFeedbackValue\b[^>]*>.*?<ValueIndex>(\d+)</ValueIndex>', $opts)
|
||||
|
||||
foreach ($m in $spMatches) {
|
||||
$idx = $m.Groups[1].Value
|
||||
if (-not $idxList.Contains($idx)) { $idxList.Add($idx) }
|
||||
}
|
||||
foreach ($m in $fbMatches) {
|
||||
$idx = $m.Groups[1].Value
|
||||
if (-not $idxList.Contains($idx)) { $idxList.Add($idx) }
|
||||
}
|
||||
return $idxList
|
||||
}
|
||||
|
||||
function Edit-GfxMainXml {
|
||||
# Modifie le contenu de Main.xml en 3 etapes :
|
||||
# 1. Collecter les IDX cibles depuis les configurations (Get-GfxSetpointIdxList)
|
||||
# 2. Modifier Minimum/Maximum dans les SmartSenseNumericValue cibles
|
||||
# 3. Modifier TemperatureSetpointLimitRelativeValue dans chaque config en mode Relatif (LimitMode=1)
|
||||
# Retourne le string XML modifie.
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$XmlContent,
|
||||
[Parameter(Mandatory)][double]$Offset,
|
||||
[Parameter(Mandatory)][string]$Hostname
|
||||
)
|
||||
|
||||
$xml = $XmlContent
|
||||
$opts = [System.Text.RegularExpressions.RegexOptions]::Singleline
|
||||
|
||||
# --- Etape 1 : collecter les IDX cibles ---
|
||||
$idxList = Get-GfxSetpointIdxList -XmlContent $xml
|
||||
|
||||
if ($idxList.Count -eq 0) {
|
||||
Write-Log -Message "[$Hostname] GFx : aucun ValueIndex de consigne trouve dans les configurations" -Level WARN
|
||||
return $xml
|
||||
}
|
||||
Write-Log -Message "[$Hostname] GFx : IDX cibles = $($idxList -join ', ')" -Level INFO
|
||||
|
||||
# --- Etape 2 : modifier Minimum/Maximum des SmartSenseNumericValue cibles ---
|
||||
$modCount = 0
|
||||
foreach ($idx in $idxList) {
|
||||
$blockPattern = '<SmartSenseNumericValue[^>]*>(?:(?!</SmartSenseNumericValue>).)*<IDX>' + $idx + '</IDX>(?:(?!</SmartSenseNumericValue>).)*</SmartSenseNumericValue>'
|
||||
$m = [regex]::Match($xml, $blockPattern, $opts)
|
||||
if ($m.Success) {
|
||||
$original = $m.Value
|
||||
$modified = [regex]::Replace($original, '<Minimum>[^<]*</Minimum>', "<Minimum>$(-$Offset)</Minimum>")
|
||||
$modified = [regex]::Replace($modified, '<Maximum>[^<]*</Maximum>', "<Maximum>$Offset</Maximum>")
|
||||
$xml = $xml.Replace($original, $modified)
|
||||
$modCount++
|
||||
Write-Log -Message "[$Hostname] GFx : IDX=$idx -> Minimum=$(-$Offset) Maximum=$Offset" -Level SUCCESS
|
||||
}
|
||||
else {
|
||||
Write-Log -Message "[$Hostname] GFx : SmartSenseNumericValue IDX=$idx introuvable dans Main.xml" -Level WARN
|
||||
}
|
||||
}
|
||||
|
||||
if ($modCount -eq 0) {
|
||||
Write-Log -Message "[$Hostname] GFx : aucun bloc Minimum/Maximum modifie" -Level WARN
|
||||
}
|
||||
|
||||
# --- Etape 3 : modifier TemperatureSetpointLimitRelativeValue par configuration ---
|
||||
$configPattern = '<IP2SmartSenseSimpleConfigurationResource[^>]*>.*?</IP2SmartSenseSimpleConfigurationResource>'
|
||||
$configMatches = [regex]::Matches($xml, $configPattern, $opts)
|
||||
|
||||
if ($configMatches.Count -eq 0) {
|
||||
Write-Log -Message "[$Hostname] GFx : aucune configuration IP2SmartSense trouvee" -Level WARN
|
||||
return $xml
|
||||
}
|
||||
|
||||
foreach ($cm in $configMatches) {
|
||||
$original = $cm.Value
|
||||
$nameMatch = [regex]::Match($original, '<NAME>(.*?)</NAME>')
|
||||
$configName = if ($nameMatch.Success) { $nameMatch.Groups[1].Value } else { "inconnue" }
|
||||
$modeMatch = [regex]::Match($original, '<TemperatureSetpointLimitMode>(\d+)</TemperatureSetpointLimitMode>')
|
||||
|
||||
if ($modeMatch.Success -and $modeMatch.Groups[1].Value -eq '1') {
|
||||
$modified = [regex]::Replace($original,
|
||||
'<TemperatureSetpointLimitRelativeValue>[^<]*</TemperatureSetpointLimitRelativeValue>',
|
||||
"<TemperatureSetpointLimitRelativeValue>$Offset</TemperatureSetpointLimitRelativeValue>")
|
||||
$xml = $xml.Replace($original, $modified)
|
||||
Write-Log -Message "[$Hostname] GFx : config '$configName' -> TemperatureSetpointLimitRelativeValue=$Offset" -Level SUCCESS
|
||||
}
|
||||
else {
|
||||
Write-Log -Message "[$Hostname] GFx : config '$configName' mode absolu - TemperatureSetpointLimitRelativeValue non modifie" -Level WARN
|
||||
}
|
||||
}
|
||||
|
||||
return $xml
|
||||
}
|
||||
|
||||
function Invoke-EditGfxFile {
|
||||
# Ouvre le ZIP GFx, remplace Main.xml par la version modifiee, recree le ZIP.
|
||||
# Tous les autres fichiers de l'archive sont preserves a l'identique.
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$InputPath,
|
||||
[Parameter(Mandatory)][string]$OutputPath,
|
||||
[Parameter(Mandatory)][double]$Offset,
|
||||
[Parameter(Mandatory)][string]$Hostname
|
||||
)
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
$zipIn = [System.IO.Compression.ZipFile]::OpenRead($InputPath)
|
||||
try {
|
||||
$mainEntry = $zipIn.GetEntry('Main.xml')
|
||||
if (-not $mainEntry) { throw "Main.xml introuvable dans l'archive GFx" }
|
||||
|
||||
$reader = New-Object System.IO.StreamReader($mainEntry.Open(), [System.Text.Encoding]::UTF8)
|
||||
$mainXml = $reader.ReadToEnd()
|
||||
$reader.Dispose()
|
||||
|
||||
$modifiedXml = Edit-GfxMainXml -XmlContent $mainXml -Offset $Offset -Hostname $Hostname
|
||||
|
||||
if (Test-Path $OutputPath) { Remove-Item $OutputPath -Force }
|
||||
$zipOut = [System.IO.Compression.ZipFile]::Open($OutputPath, [System.IO.Compression.ZipArchiveMode]::Create)
|
||||
try {
|
||||
foreach ($entry in $zipIn.Entries) {
|
||||
$newEntry = $zipOut.CreateEntry($entry.FullName, [System.IO.Compression.CompressionLevel]::Optimal)
|
||||
$outStream = $newEntry.Open()
|
||||
if ($entry.FullName -eq 'Main.xml') {
|
||||
$xmlBytes = [System.Text.Encoding]::UTF8.GetBytes($modifiedXml)
|
||||
$outStream.Write($xmlBytes, 0, $xmlBytes.Length)
|
||||
}
|
||||
else {
|
||||
$inStream = $entry.Open()
|
||||
$inStream.CopyTo($outStream)
|
||||
$inStream.Dispose()
|
||||
}
|
||||
$outStream.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$zipOut.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$zipIn.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
# ============================================================
|
||||
#region MAIN
|
||||
# ============================================================
|
||||
|
||||
$bleDevicePath = "/api/rest/v2/services/subnet/devices/ble-room-device"
|
||||
$gfxFilePath = "/api/rest/v2/services/gfx/files/project/Project.gfx"
|
||||
|
||||
Initialize-Logger
|
||||
Initialize-ApiClient
|
||||
|
||||
$gfxMode = if ($UpdateGfx) { "BLE + GFx" } else { "BLE uniquement" }
|
||||
Write-Log -Message "=== EditOffsetCLI demarre - Offset: $Offset - Mode: $gfxMode ===" -Level INFO
|
||||
|
||||
$csvRows = Read-AutomateCsv -CsvPath $CsvInput
|
||||
$uniqueAutomates = $csvRows | Sort-Object -Property "Current Ip" -Unique
|
||||
|
||||
foreach ($automate in $uniqueAutomates) {
|
||||
$hostname = $automate.Hostname
|
||||
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
|
||||
}
|
||||
|
||||
$creds = Get-Credentials -Automate $automate -DefaultUsername $Username -DefaultPassword $Password
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 1. GET device complet — identifie local-values et configurations
|
||||
# ----------------------------------------------------------
|
||||
$deviceJson = Invoke-ApiGet -Url "$baseUrl$bleDevicePath/" `
|
||||
-Username $creds.Username -Password $creds.Password
|
||||
|
||||
if (-not (Test-HasRelativeSetpoint -JsonContent $deviceJson)) {
|
||||
Write-Log -Message "[$hostname] Mode absolu detecte (pas de relative-value) - aucune modification effectuee" -Level WARN
|
||||
continue
|
||||
}
|
||||
|
||||
$lvKeys = Get-SetpointLocalValueKeys -JsonContent $deviceJson
|
||||
$configIds = Get-AllConfigIds -JsonContent $deviceJson
|
||||
|
||||
if ($lvKeys.Count -eq 0) {
|
||||
Write-Log -Message "[$hostname] Aucune local-value de consigne trouvee - ignore" -Level WARN
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Log -Message "[$hostname] Local-values a modifier : $($lvKeys -join ', ')" -Level INFO
|
||||
Write-Log -Message "[$hostname] Configurations a modifier : $($configIds -join ', ')" -Level INFO
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 2. Modifier high-limit et low-limit de chaque local-value
|
||||
# ----------------------------------------------------------
|
||||
foreach ($key in $lvKeys) {
|
||||
try {
|
||||
$url = "$baseUrl$bleDevicePath/local-values/$key/"
|
||||
$lvJson = Invoke-ApiGet -Url $url -Username $creds.Username -Password $creds.Password
|
||||
|
||||
$lv = $lvJson | ConvertFrom-Json
|
||||
$lv."high-limit" = $Offset
|
||||
$lv."low-limit" = -$Offset
|
||||
$body = $lv | ConvertTo-Json -Depth 20 -Compress
|
||||
|
||||
Invoke-ApiPost -Url $url -JsonBody $body -Username $creds.Username -Password $creds.Password
|
||||
|
||||
Update-Stats -Counter LocalValuesUpdated
|
||||
Write-Log -Message "[$hostname] local-value $key : high-limit=$Offset low-limit=$(-$Offset) OK" -Level SUCCESS
|
||||
}
|
||||
catch {
|
||||
Write-Log -Message "[$hostname] local-value $key ERREUR : $($_.Exception.Message)" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 3. Modifier relative-value de chaque configuration
|
||||
# ----------------------------------------------------------
|
||||
foreach ($configId in $configIds) {
|
||||
try {
|
||||
$url = "$baseUrl$bleDevicePath/configurations/$configId"
|
||||
$cfgJson = Invoke-ApiGet -Url $url -Username $creds.Username -Password $creds.Password
|
||||
|
||||
$cfg = $cfgJson | ConvertFrom-Json
|
||||
if (-not $cfg."setpoint-temperature") {
|
||||
Write-Log -Message "[$hostname] config $configId : pas de setpoint-temperature - ignore" -Level WARN
|
||||
continue
|
||||
}
|
||||
|
||||
if ($cfg."setpoint-temperature".PSObject.Properties.Name -notcontains "relative-value") {
|
||||
Write-Log -Message "[$hostname] config $configId : mode absolu - ignore" -Level WARN
|
||||
continue
|
||||
}
|
||||
|
||||
$cfg."setpoint-temperature"."relative-value" = $Offset
|
||||
$body = $cfg | ConvertTo-Json -Depth 20 -Compress
|
||||
|
||||
Invoke-ApiPost -Url $url -JsonBody $body -Username $creds.Username -Password $creds.Password
|
||||
|
||||
Update-Stats -Counter ConfigsUpdated
|
||||
Write-Log -Message "[$hostname] config $configId : relative-value=$Offset OK" -Level SUCCESS
|
||||
}
|
||||
catch {
|
||||
Write-Log -Message "[$hostname] config $configId ERREUR : $($_.Exception.Message)" -Level ERROR
|
||||
}
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 4. Modifier le fichier GFx (uniquement si -UpdateGfx)
|
||||
# ----------------------------------------------------------
|
||||
if ($UpdateGfx) {
|
||||
$tempGfxIn = [System.IO.Path]::GetTempFileName() + ".gfx"
|
||||
$tempGfxOut = [System.IO.Path]::GetTempFileName() + ".gfx"
|
||||
try {
|
||||
$gfxBytes = Invoke-ApiGetBinary -Url "$baseUrl${gfxFilePath}?content=media" `
|
||||
-Username $creds.Username -Password $creds.Password
|
||||
[System.IO.File]::WriteAllBytes($tempGfxIn, $gfxBytes)
|
||||
|
||||
Invoke-EditGfxFile -InputPath $tempGfxIn -OutputPath $tempGfxOut `
|
||||
-Offset $Offset -Hostname $hostname
|
||||
|
||||
$newBytes = [System.IO.File]::ReadAllBytes($tempGfxOut)
|
||||
Invoke-ApiPostBinary -Url "$baseUrl${gfxFilePath}?overwrite=true" -Bytes $newBytes `
|
||||
-Username $creds.Username -Password $creds.Password
|
||||
|
||||
Update-Stats -Counter GfxUpdated
|
||||
Write-Log -Message "[$hostname] GFx modifie et uploade avec succes" -Level SUCCESS
|
||||
}
|
||||
catch {
|
||||
if ($_.Exception.Message -match "404|Not Found") {
|
||||
Write-Log -Message "[$hostname] GFx introuvable sur cet automate - ignore" -Level WARN
|
||||
}
|
||||
else {
|
||||
Write-Log -Message "[$hostname] GFx ERREUR : $($_.Exception.Message)" -Level ERROR
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tempGfxIn -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $tempGfxOut -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log -Message "[$hostname] ERREUR FATALE : $($_.Exception.Message)" -Level ERROR
|
||||
Update-Stats -Counter AutomatesError
|
||||
}
|
||||
}
|
||||
|
||||
Write-Summary
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,236 @@
|
||||
# EditOffsetCLI — Modification de la consigne relative des BLE Device (Multi-capteur/Unitouch) en masse
|
||||
|
||||
> **IMPORTANT : Ce projet est un développement personnel indépendant.**
|
||||
>
|
||||
> Ce projet a été développé par Charles-Arthur DAVID à titre personnel.
|
||||
> Distech Controls n'est pas responsable de ce projet et ne le supporte pas.
|
||||
> Aucune demande de support ne sera prise en charge par Distech Controls.
|
||||
> Distech Controls ne fournit aucune garantie ni assistance technique pour ce projet.
|
||||
|
||||
---
|
||||
|
||||
Outil de configuration automatique de la plage de réglage de consigne relative des BLE Device (Multi-capteur/Unitouch) sur automates Distech Controls Eclypse.
|
||||
|
||||
---
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Windows 10 ou Windows 11
|
||||
- Automates **Distech Controls ECY Facilities (V2)**
|
||||
- Être connecté au même réseau que les automates (réseau local ou VPN)
|
||||
- Avoir les identifiants de connexion aux automates (nom d'utilisateur et mot de passe)
|
||||
|
||||
Aucune installation de logiciel supplémentaire n'est nécessaire.
|
||||
|
||||
---
|
||||
|
||||
## Contenu du dossier
|
||||
|
||||
```
|
||||
EditOffsetCLI.ps1 ← script principal (fichier unique à exécuter)
|
||||
votre_liste.csv ← export XNU ou fichier CSV de votre choix (voir ci-dessous)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 1 — Débloquer le script (première utilisation uniquement)
|
||||
|
||||
Windows bloque par sécurité les fichiers téléchargés depuis internet. Cette étape n'est à faire **qu'une seule fois**.
|
||||
|
||||
1. Faites un **clic droit** sur le fichier `EditOffsetCLI.ps1`
|
||||
2. Cliquez sur **Propriétés**
|
||||
3. En bas de la fenêtre, cochez la case **Débloquer**
|
||||
4. Cliquez sur **OK**
|
||||
|
||||

|
||||
|
||||
> Si vous ne voyez pas la case "Débloquer", le fichier est déjà autorisé, passez à l'étape suivante.
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 — Préparer la liste des automates (fichier CSV)
|
||||
|
||||
### Option A — Utiliser un export XpressNetwork Utility (recommandé)
|
||||
|
||||
Si vous disposez de **XpressNetwork Utility**, exportez directement votre liste d'automates en CSV : le fichier est utilisable tel quel sans modification.
|
||||
|
||||
### Option B — Créer le fichier manuellement
|
||||
|
||||
Créez un fichier texte nommé `automates.csv` dans le même dossier que le script.
|
||||
|
||||
Ouvrez-le avec **le Bloc-notes** (pas Excel) et collez ce contenu en adaptant vos données :
|
||||
|
||||
```
|
||||
Hostname;Current Ip
|
||||
Automate-Bureau-101;192.168.1.10
|
||||
Automate-Bureau-102;192.168.1.11
|
||||
Automate-Salle-Conf;192.168.1.12
|
||||
```
|
||||
|
||||
### Détail des colonnes
|
||||
|
||||
| Colonne | Obligatoire | Description | Exemple |
|
||||
|---------|:-----------:|-------------|---------|
|
||||
| `Current Ip` | ✅ | Adresse IP de l'automate | `192.168.1.10` |
|
||||
| `Hostname` | ❌ | Nom libre pour identifier l'automate dans les logs | `Automate-Bureau-101` |
|
||||
| `Username` | ❌ | Identifiant si différent du paramètre `-Username` | `technicien` |
|
||||
| `Password` | ❌ | Mot de passe si différent du paramètre `-Password` | `motdepasse` |
|
||||
|
||||
> Les automates ECY Facilities V2 communiquent toujours en HTTPS — le script le gère automatiquement.
|
||||
|
||||
### Identifiants différents par automate (optionnel)
|
||||
|
||||
Si certains automates ont des identifiants différents, ajoutez les colonnes `Username` et `Password` :
|
||||
|
||||
```
|
||||
Hostname;Current Ip;Username;Password
|
||||
Automate-Bureau-101;192.168.1.10;admin;motdepasse1
|
||||
Automate-Bureau-102;192.168.1.11;technicien;motdepasse2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 — Ouvrir PowerShell dans le bon dossier
|
||||
|
||||
1. Ouvrez l'**Explorateur de fichiers** et naviguez jusqu'au dossier contenant `EditOffsetCLI.ps1`
|
||||
2. Cliquez dans la **barre d'adresse** en haut (là où est affiché le chemin du dossier)
|
||||
3. Tapez `powershell` puis appuyez sur **Entrée**
|
||||
|
||||
Une fenêtre PowerShell s'ouvre directement dans le bon dossier.
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 — Lancer le script
|
||||
|
||||
Dans la fenêtre PowerShell, tapez la commande suivante en remplaçant `3` par la valeur d'offset souhaitée :
|
||||
|
||||
```powershell
|
||||
.\EditOffsetCLI.ps1 -CsvInput ".\automates.csv" -Offset 3
|
||||
```
|
||||
|
||||
### Avec identifiants (si tous les automates ont le même mot de passe)
|
||||
|
||||
```powershell
|
||||
.\EditOffsetCLI.ps1 -CsvInput ".\automates.csv" -Offset 3 -Username "admin" -Password "votre_mot_de_passe"
|
||||
```
|
||||
|
||||
### Paramètres disponibles
|
||||
|
||||
| Paramètre | Obligatoire | Description |
|
||||
|-----------|:-----------:|-------------|
|
||||
| `-CsvInput` | ✅ | Chemin vers votre fichier CSV |
|
||||
| `-Offset` | ✅ | Valeur de l'offset (ex: `3` → plage de -3 à +3°C) |
|
||||
| `-Username` | ❌ | Identifiant API (défaut : `admin`) |
|
||||
| `-Password` | ❌ | Mot de passe API (défaut : vide) |
|
||||
| `-UpdateGfx` | ❌ | Modifier aussi le fichier GFx de l'automate (interface graphique) |
|
||||
|
||||
> **Que fait l'offset ?** Si vous entrez `-Offset 3`, le script configure :
|
||||
> - Limite haute : **+3°C**
|
||||
> - Limite basse : **-3°C**
|
||||
> - Consigne relative : **3°C**
|
||||
|
||||
### Avec mise à jour du fichier GFx (interface graphique)
|
||||
|
||||
Pour synchroniser aussi les limites dans l'interface graphique de l'automate, ajoutez `-UpdateGfx` :
|
||||
|
||||
```powershell
|
||||
.\EditOffsetCLI.ps1 -CsvInput ".\automates.csv" -Offset 3 -UpdateGfx
|
||||
```
|
||||
|
||||
> Sans `-UpdateGfx`, seule la configuration BLE est modifiée. Recommandé de tester sans `-UpdateGfx` d'abord.
|
||||
|
||||
---
|
||||
|
||||
## Étape 5 — Suivre l'exécution
|
||||
|
||||
Pendant l'exécution, le script affiche les opérations en temps réel avec des couleurs :
|
||||
|
||||
| Couleur | Signification |
|
||||
|---------|---------------|
|
||||
| 🔵 Cyan | Information (opération en cours) |
|
||||
| 🟢 Vert | Succès |
|
||||
| 🟡 Jaune | Avertissement (automate ignoré, port invalide…) |
|
||||
| 🔴 Rouge | Erreur |
|
||||
|
||||
À la fin, un résumé s'affiche :
|
||||
|
||||
```
|
||||
[RESUME]
|
||||
Automates traites : 5
|
||||
Automates en erreur : 0
|
||||
Local-values mises a jour : 10
|
||||
Configurations mises a jour : 5
|
||||
GFx mis a jour : 5
|
||||
Duree totale : 00:00:12.34
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fichier journal (log)
|
||||
|
||||
Un fichier de log est créé automatiquement dans le même dossier, nommé :
|
||||
|
||||
```
|
||||
editoffset_2026-06-24_14h30.log
|
||||
```
|
||||
|
||||
Ce fichier contient l'historique complet de toutes les opérations effectuées. En cas de problème, transmettez ce fichier à votre référent technique.
|
||||
|
||||
---
|
||||
|
||||
## Problèmes courants
|
||||
|
||||
### ❌ "L'exécution de scripts est désactivée sur ce système"
|
||||
|
||||
Windows bloque l'exécution des scripts par politique de sécurité. Lancez cette commande dans PowerShell **en tant qu'administrateur** :
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
Puis relancez le script normalement.
|
||||
|
||||
---
|
||||
|
||||
### ❌ "Fichier CSV introuvable"
|
||||
|
||||
Vérifiez que :
|
||||
- Le fichier `automates.csv` est bien dans le **même dossier** que `EditOffsetCLI.ps1`
|
||||
- Le nom du fichier est exactement `automates.csv` (pas `automates.csv.txt`)
|
||||
|
||||
> Dans l'Explorateur de fichiers, activez **Affichage → Extensions de noms de fichiers** pour voir l'extension réelle.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Tous les automates sont en erreur
|
||||
|
||||
Vérifiez que :
|
||||
- Votre PC est bien connecté au réseau des automates
|
||||
- Les adresses IP dans le CSV sont correctes
|
||||
- Le port renseigné correspond au protocole actif sur l'automate (HTTP ou HTTPS)
|
||||
- Les identifiants sont corrects
|
||||
|
||||
---
|
||||
|
||||
### ❌ "Erreur SSL/TLS" ou "certificat"
|
||||
|
||||
Le script gère automatiquement les certificats auto-signés des automates Distech. Si l'erreur persiste, vérifiez que `curl.exe` est disponible sur votre système (inclus dans Windows 10 v1803+ et Windows 11).
|
||||
|
||||
---
|
||||
|
||||
## Exemple complet
|
||||
|
||||
Fichier `automates.csv` :
|
||||
```
|
||||
Hostname;Current Ip
|
||||
Salle-A101;10.0.1.50
|
||||
Salle-A102;10.0.1.51
|
||||
Salle-B201;10.0.1.52
|
||||
```
|
||||
|
||||
Commande :
|
||||
```powershell
|
||||
.\EditOffsetCLI.ps1 -CsvInput ".\automates.csv" -Offset 4 -Username "admin" -Password "Distech2024"
|
||||
```
|
||||
|
||||
Résultat attendu : les 3 automates sont configurés avec une plage de -4°C à +4°C.
|
||||
Reference in New Issue
Block a user