ViewPower nu putea dialoga cu acest UPS: 5777 de "QPI return(NAK" si zero citiri reusite in fereastra 6-10 august, pentru ca UPS-ul vorbeste HID Power Device standard, nu dialectul text Voltronic. Serviciile au fost dezactivate pe 10.08.2026 la 22:34, lasand serverul fara nicio alertare 17 zile. Nici inainte nu exista: emailReceivers = 0, iar excuteProgram era gol, deci Oracle nu era oprit curat oricum. UPS-ul comunica insa perfect cu Windows pe canalul HID - dovedit de perechile de evenimente Kernel-Power 105 la panele reale din 2026. Monitorul nou citeste GetSystemPowerStatus, acelasi semnal pe care il foloseste Windows pentru propria actiune la baterie critica. Alerteaza pe email la trecerea pe baterie, la revenirea curentului si cand UPS-ul nu mai comunica - cazul care a trecut neobservat. Opreste curat listenerul, apoi instanta cu shutdown immediate, apoi Windows-ul, inaintea pragului brutal de 20% al Windows-ului. Ruleaza ca task programat sub SYSTEM, cu AllowStartIfOnBatteries si DontStopIfGoingOnBatteries - fara ele Windows ar fi oprit taskul exact cand serverul trece pe baterie. Validat pe 27.08.2026: citire, email, secventa de oprire in dry-run si pierderea comunicatiei (alerta la exact 8 cicluri, apoi revenire). Pragurile de 10 min / 35% sunt conservatoare, nu masurate - testul de autonomie reala ramane de facut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RRyaDj39hPQ89SZS6URRpS
606 lines
23 KiB
PowerShell
606 lines
23 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Monitorizeaza UPS-ul serverului Oracle de productie (10.0.20.36 / ROA-CARAPETRU2)
|
|
prin canalul HID nativ Windows si opreste curat baza inainte de epuizarea bateriei.
|
|
|
|
.DESCRIPTION
|
|
Inlocuieste ViewPower, care nu reusea handshake-ul de protocol cu acest UPS
|
|
(5777 x "QPI return(NAK", zero citiri reusite) si a fost dezactivat pe 10.08.2026.
|
|
|
|
Sursa de date este API-ul Windows GetSystemPowerStatus - exact acelasi semnal pe
|
|
care il foloseste si Windows pentru actiunea proprie de "critical battery", deci
|
|
daca merge oprirea automata a Windows-ului merge si scriptul asta.
|
|
|
|
Ce face:
|
|
- alerteaza pe email la trecerea pe baterie si la revenirea curentului;
|
|
- alerteaza cand UPS-ul nu mai comunica deloc cu calculatorul (cazul care a
|
|
trecut neobservat 17 zile in august 2026);
|
|
- opreste curat Oracle (listener, apoi "shutdown immediate") si abia apoi
|
|
Windows-ul, inainte ca bateria sa ajunga la pragul brutal al Windows-ului.
|
|
|
|
Documentatie: docs/ups-server36-monitorizare.md
|
|
|
|
.PARAMETER ConfigPath
|
|
Calea catre config.json. Implicit, langa script.
|
|
|
|
.PARAMETER Mode
|
|
Run - bucla continua (modul in care ruleaza taskul programat).
|
|
Once - o singura evaluare, apoi iese. Pentru depanare.
|
|
Status - afiseaza starea curenta (UPS, Oracle, task, ultimele evenimente).
|
|
TestMail - trimite un email de test si iese.
|
|
TestShutdown - parcurge secventa de oprire in gol (dry-run), fara sa opreasca nimic.
|
|
|
|
.NOTES
|
|
Fisierul e intentionat fara diacritice: PowerShell 5.1 citeste .ps1 fara BOM ca
|
|
ANSI, iar diacriticele ar ajunge corupte in emailuri.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
# Implicit, config.json de langa script. Nu se poate folosi $PSScriptRoot direct
|
|
# in valoarea implicita: la legarea parametrilor e inca gol.
|
|
[string]$ConfigPath,
|
|
|
|
[ValidateSet('Run', 'Once', 'Status', 'TestMail', 'TestShutdown')]
|
|
[string]$Mode = 'Run'
|
|
)
|
|
|
|
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
if (-not $ConfigPath) {
|
|
$scriptDir = $PSScriptRoot
|
|
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
|
|
$ConfigPath = Join-Path $scriptDir 'config.json'
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API-ul de alimentare al Windows-ului
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if (-not ('RomfastPowerStatus' -as [type])) {
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct SYSTEM_POWER_STATUS {
|
|
public byte ACLineStatus;
|
|
public byte BatteryFlag;
|
|
public byte BatteryLifePercent;
|
|
public byte SystemStatusFlag;
|
|
public int BatteryLifeTime;
|
|
public int BatteryFullLifeTime;
|
|
}
|
|
|
|
public static class RomfastPowerStatus {
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
public static extern bool GetSystemPowerStatus(out SYSTEM_POWER_STATUS status);
|
|
}
|
|
'@
|
|
}
|
|
|
|
# ACLineStatus: 0 = pe baterie, 1 = pe retea, 255 = necunoscut
|
|
# BatteryFlag: 1 = high, 2 = low, 4 = critical, 8 = charging,
|
|
# 128 = NU EXISTA BATERIE (UPS-ul nu mai comunica), 255 = necunoscut
|
|
# BatteryLifePercent: 0..100, 255 = necunoscut
|
|
# BatteryLifeTime: secunde ramase, -1 = necunoscut
|
|
|
|
function Get-PowerReading {
|
|
$st = New-Object SYSTEM_POWER_STATUS
|
|
$ok = [RomfastPowerStatus]::GetSystemPowerStatus([ref]$st)
|
|
if (-not $ok) {
|
|
throw "GetSystemPowerStatus a esuat (Win32 error $([Runtime.InteropServices.Marshal]::GetLastWin32Error()))"
|
|
}
|
|
|
|
$noBattery = ($st.BatteryFlag -eq 128) -or ($st.BatteryFlag -eq 255)
|
|
|
|
$percent = $null
|
|
if ($st.BatteryLifePercent -le 100) { $percent = [int]$st.BatteryLifePercent }
|
|
|
|
$runtime = $null
|
|
if ($st.BatteryLifeTime -ge 0) { $runtime = [int]$st.BatteryLifeTime }
|
|
|
|
[pscustomobject]@{
|
|
UpsPresent = (-not $noBattery)
|
|
OnBattery = ($st.ACLineStatus -eq 0)
|
|
AcKnown = ($st.ACLineStatus -eq 0 -or $st.ACLineStatus -eq 1)
|
|
Percent = $percent
|
|
RuntimeSec = $runtime
|
|
ACLineStatus = [int]$st.ACLineStatus
|
|
BatteryFlag = [int]$st.BatteryFlag
|
|
}
|
|
}
|
|
|
|
function Get-UpsDeviceInfo {
|
|
# Confirmare a doua, din WMI - folosita doar in alerte si in modul Status,
|
|
# ca sa nu interogam WMI la fiecare ciclu.
|
|
try {
|
|
$b = Get-CimInstance Win32_Battery -ErrorAction Stop | Select-Object -First 1
|
|
if ($null -eq $b) { return 'Win32_Battery: nicio instanta (UPS-ul nu e vazut de Windows)' }
|
|
return "Win32_Battery: $($b.Name), Status=$($b.Status), BatteryStatus=$($b.BatteryStatus), Incarcare=$($b.EstimatedChargeRemaining)%"
|
|
} catch {
|
|
return "Win32_Battery: interogare esuata ($($_.Exception.Message))"
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuratie, log, stare
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Get-Config {
|
|
param([string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
throw "Lipseste fisierul de configurare: $Path"
|
|
}
|
|
$cfg = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
|
|
# Valori implicite pentru cheile care pot lipsi.
|
|
$defaults = @{
|
|
enabled = $true
|
|
pollSeconds = 15
|
|
shutdownAfterMinutesOnBattery = 10
|
|
shutdownAtBatteryPercent = 35
|
|
shutdownAtRuntimeSeconds = 0 # 0 = criteriu dezactivat
|
|
confirmPolls = 3
|
|
commsLostPolls = 8
|
|
commsAlertRepeatHours = 24
|
|
}
|
|
foreach ($k in $defaults.Keys) {
|
|
if (-not ($cfg.PSObject.Properties.Name -contains $k)) {
|
|
$cfg | Add-Member -NotePropertyName $k -NotePropertyValue $defaults[$k]
|
|
}
|
|
}
|
|
return $cfg
|
|
}
|
|
|
|
$script:LogDir = $null
|
|
|
|
function Write-Log {
|
|
param(
|
|
[string]$Message,
|
|
[ValidateSet('INFO', 'WARN', 'ALERT', 'ERROR')]
|
|
[string]$Level = 'INFO'
|
|
)
|
|
$line = '{0} {1,-5} {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message
|
|
Write-Host $line
|
|
if ($script:LogDir) {
|
|
try {
|
|
if (-not (Test-Path -LiteralPath $script:LogDir)) {
|
|
New-Item -ItemType Directory -Path $script:LogDir -Force | Out-Null
|
|
}
|
|
$file = Join-Path $script:LogDir ('ups-monitor-{0}.log' -f (Get-Date -Format 'yyyy-MM'))
|
|
Add-Content -LiteralPath $file -Value $line -Encoding UTF8
|
|
} catch {
|
|
# Logul nu are voie sa opreasca monitorizarea.
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-State {
|
|
param([string]$Path)
|
|
if (Test-Path -LiteralPath $Path) {
|
|
try {
|
|
$loaded = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
return (Initialize-State -State $loaded)
|
|
} catch {
|
|
Write-Log "Fisierul de stare e corupt, se reia de la zero: $($_.Exception.Message)" -Level WARN
|
|
}
|
|
}
|
|
return New-State
|
|
}
|
|
|
|
function New-State {
|
|
[pscustomobject]@{
|
|
powerState = 'unknown' # online | battery | unknown
|
|
onBatterySince = $null
|
|
commsState = 'unknown' # ok | lost | unknown
|
|
lastCommsAlert = $null
|
|
shutdownTriggered = $false
|
|
}
|
|
}
|
|
|
|
function Initialize-State {
|
|
# Un state.json scris de o versiune mai veche poate sa nu aiba toate cheile,
|
|
# iar Set-StrictMode ar pica pe prima proprietate lipsa.
|
|
param([object]$State)
|
|
$template = New-State
|
|
foreach ($p in $template.PSObject.Properties) {
|
|
if (-not ($State.PSObject.Properties.Name -contains $p.Name)) {
|
|
$State | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value
|
|
}
|
|
}
|
|
return $State
|
|
}
|
|
|
|
function Save-State {
|
|
param([object]$State, [string]$Path)
|
|
try {
|
|
$dir = Split-Path -Parent $Path
|
|
if ($dir -and -not (Test-Path -LiteralPath $dir)) {
|
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
}
|
|
$State | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $Path -Encoding UTF8
|
|
} catch {
|
|
Write-Log "Nu am putut salva starea in $Path : $($_.Exception.Message)" -Level WARN
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Email
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Send-UpsMail {
|
|
param(
|
|
[object]$Config,
|
|
[string]$Subject,
|
|
[string]$Body
|
|
)
|
|
# Un email esuat nu are voie sa opreasca bucla si nici secventa de oprire:
|
|
# in pana de curent e foarte posibil sa fie cazut si switch-ul.
|
|
try {
|
|
$smtp = New-Object System.Net.Mail.SmtpClient($Config.smtp.server, [int]$Config.smtp.port)
|
|
$smtp.EnableSsl = $true
|
|
$smtp.Timeout = 20000
|
|
# UseDefaultCredentials trebuie pus inainte de Credentials: setarea lui
|
|
# sterge acreditarile deja atribuite.
|
|
$smtp.UseDefaultCredentials = $false
|
|
$smtp.Credentials = New-Object System.Net.NetworkCredential($Config.smtp.user, $Config.smtp.password)
|
|
|
|
$msg = New-Object System.Net.Mail.MailMessage
|
|
$msg.From = New-Object System.Net.Mail.MailAddress($Config.smtp.from, "UPS $env:COMPUTERNAME")
|
|
foreach ($to in $Config.mailTo) { $msg.To.Add($to) }
|
|
$msg.Subject = $Subject
|
|
$msg.Body = $Body
|
|
$msg.SubjectEncoding = [Text.Encoding]::UTF8
|
|
$msg.BodyEncoding = [Text.Encoding]::UTF8
|
|
|
|
$smtp.Send($msg)
|
|
$msg.Dispose()
|
|
$smtp.Dispose()
|
|
Write-Log "Email trimis: $Subject"
|
|
return $true
|
|
} catch {
|
|
Write-Log "Email NETRIMIS ($Subject): $($_.Exception.Message)" -Level ERROR
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Format-Reading {
|
|
param([object]$Reading, [object]$Config)
|
|
|
|
$pct = 'necunoscut'
|
|
if ($null -ne $Reading.Percent) { $pct = "$($Reading.Percent)%" }
|
|
|
|
$rt = 'neraportat de UPS'
|
|
if ($null -ne $Reading.RuntimeSec) { $rt = '{0} min' -f [math]::Round($Reading.RuntimeSec / 60, 1) }
|
|
|
|
$alim = 'necunoscuta'
|
|
if ($Reading.OnBattery) { $alim = 'PE BATERIE' } elseif ($Reading.AcKnown) { $alim = 'pe retea' }
|
|
|
|
$vazut = 'NU'
|
|
if ($Reading.UpsPresent) { $vazut = 'da' }
|
|
|
|
$oracle = 'nedeterminat'
|
|
try {
|
|
$svc = Get-Service -Name $Config.oracle.service -ErrorAction Stop
|
|
$lsn = Get-Service -Name $Config.oracle.listenerService -ErrorAction Stop
|
|
$oracle = "$($svc.Name)=$($svc.Status), $($lsn.Name)=$($lsn.Status)"
|
|
} catch {
|
|
$oracle = "nedeterminat ($($_.Exception.Message))"
|
|
}
|
|
|
|
@"
|
|
Server : $env:COMPUTERNAME (10.0.20.36)
|
|
Moment : $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')
|
|
Alimentare : $alim
|
|
Incarcare : $pct
|
|
Autonomie estim. : $rt
|
|
UPS vazut de OS : $vazut
|
|
$(Get-UpsDeviceInfo)
|
|
Oracle : $oracle
|
|
|
|
Praguri configurate:
|
|
oprire dupa $($Config.shutdownAfterMinutesOnBattery) min pe baterie
|
|
oprire sub $($Config.shutdownAtBatteryPercent)% incarcare
|
|
Windows opreste singur, ca ultima plasa, la 20%
|
|
"@
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Oprirea curata
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Stop-OracleCleanly {
|
|
param([object]$Config, [switch]$DryRun)
|
|
|
|
$oraHome = $Config.oracle.home
|
|
$sid = $Config.oracle.sid
|
|
$timeoutMs = [int]$Config.oracle.shutdownTimeoutSeconds * 1000
|
|
|
|
Write-Log "Opresc listenerul $($Config.oracle.listenerService)"
|
|
if (-not $DryRun) {
|
|
try {
|
|
Stop-Service -Name $Config.oracle.listenerService -Force -ErrorAction Stop
|
|
Write-Log 'Listener oprit'
|
|
} catch {
|
|
Write-Log "Listenerul nu s-a oprit: $($_.Exception.Message)" -Level WARN
|
|
}
|
|
}
|
|
|
|
$sqlplus = Join-Path $oraHome 'bin\sqlplus.exe'
|
|
if (-not (Test-Path -LiteralPath $sqlplus)) {
|
|
Write-Log "sqlplus.exe lipseste la $sqlplus - trec direct pe oprirea serviciului" -Level WARN
|
|
} else {
|
|
Write-Log "Oracle: shutdown immediate pe instanta $sid (timeout $($Config.oracle.shutdownTimeoutSeconds)s)"
|
|
if (-not $DryRun) {
|
|
$tmpSql = Join-Path $env:TEMP ('ups-shutdown-{0}.sql' -f $PID)
|
|
$tmpOut = Join-Path $env:TEMP ('ups-shutdown-{0}.out' -f $PID)
|
|
try {
|
|
Set-Content -LiteralPath $tmpSql -Encoding ASCII -Value @(
|
|
'whenever sqlerror exit failure'
|
|
'shutdown immediate'
|
|
'exit'
|
|
)
|
|
$env:ORACLE_HOME = $oraHome
|
|
$env:ORACLE_SID = $sid
|
|
|
|
$p = Start-Process -FilePath $sqlplus `
|
|
-ArgumentList @('-S', '-L', '/ as sysdba', "@$tmpSql") `
|
|
-NoNewWindow -PassThru `
|
|
-RedirectStandardOutput $tmpOut -RedirectStandardError "$tmpOut.err"
|
|
|
|
if ($p.WaitForExit($timeoutMs)) {
|
|
$out = if (Test-Path $tmpOut) { (Get-Content -LiteralPath $tmpOut -Raw).Trim() } else { '' }
|
|
Write-Log "sqlplus a iesit cu codul $($p.ExitCode). Iesire: $out"
|
|
} else {
|
|
Write-Log 'sqlplus nu a terminat in timpul alocat - il opresc si merg pe serviciu' -Level WARN
|
|
try { $p.Kill() } catch { }
|
|
}
|
|
} catch {
|
|
Write-Log "shutdown immediate a esuat: $($_.Exception.Message)" -Level WARN
|
|
} finally {
|
|
Remove-Item -LiteralPath $tmpSql, $tmpOut, "$tmpOut.err" -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
# Plasa de siguranta: serviciul e configurat el insusi cu ORA_ROA_SHUTDOWNTYPE=immediate
|
|
# si ORA_ROA_SHUTDOWN_TIMEOUT=90, deci oprirea lui e tot curata.
|
|
Write-Log "Opresc serviciul $($Config.oracle.service)"
|
|
if (-not $DryRun) {
|
|
try {
|
|
Stop-Service -Name $Config.oracle.service -Force -ErrorAction Stop
|
|
Write-Log 'Serviciul Oracle oprit'
|
|
} catch {
|
|
Write-Log "Serviciul Oracle nu s-a oprit: $($_.Exception.Message)" -Level ERROR
|
|
}
|
|
}
|
|
}
|
|
|
|
function Invoke-UpsShutdown {
|
|
param(
|
|
[object]$Config,
|
|
[object]$Reading,
|
|
[string]$Reason,
|
|
[switch]$DryRun
|
|
)
|
|
|
|
$tag = if ($DryRun) { '[DRY-RUN] ' } else { '' }
|
|
Write-Log "${tag}DECLANSEZ OPRIREA: $Reason" -Level ALERT
|
|
|
|
# Emailul pleaca inainte de orice - in pana de curent reteaua poate cadea oricand.
|
|
Send-UpsMail -Config $Config `
|
|
-Subject "$tag[UPS] OPRESC serverul $env:COMPUTERNAME - $Reason" `
|
|
-Body ("Motiv: $Reason`n`n" + (Format-Reading -Reading $Reading -Config $Config) + @"
|
|
|
|
Secventa care urmeaza:
|
|
1. oprire listener Oracle
|
|
2. shutdown immediate pe instanta $($Config.oracle.sid)
|
|
3. oprire serviciu Oracle
|
|
4. shutdown Windows
|
|
|
|
La revenirea curentului serverul NU porneste singur decat daca e activat
|
|
"Restore on AC power loss" in BIOS.
|
|
"@) | Out-Null
|
|
|
|
Stop-OracleCleanly -Config $Config -DryRun:$DryRun
|
|
|
|
Write-Log "${tag}Opresc Windows-ul"
|
|
if (-not $DryRun) {
|
|
# Out-Null: altfel iesirea lui shutdown.exe s-ar amesteca in valoarea
|
|
# returnata de Invoke-Cycle, care e obiectul de stare.
|
|
& shutdown.exe /s /f /t 30 /c "UPS: $Reason" | Out-Null
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Un ciclu de evaluare
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Invoke-Cycle {
|
|
param([object]$Config, [object]$State, [switch]$DryRun)
|
|
|
|
$now = Get-Date
|
|
$r = Get-PowerReading
|
|
|
|
# --- comunicatia cu UPS-ul ---------------------------------------------
|
|
if (-not $r.UpsPresent) {
|
|
$script:CommsMissCount++
|
|
if ($script:CommsMissCount -ge $Config.commsLostPolls) {
|
|
$repeat = $false
|
|
if ($State.lastCommsAlert) {
|
|
$repeat = ((New-TimeSpan -Start ([datetime]$State.lastCommsAlert) -End $now).TotalHours -ge $Config.commsAlertRepeatHours)
|
|
}
|
|
if ($State.commsState -ne 'lost' -or $repeat) {
|
|
Write-Log 'UPS-ul nu mai este vazut de Windows' -Level ALERT
|
|
Send-UpsMail -Config $Config `
|
|
-Subject "[UPS] $env:COMPUTERNAME nu mai comunica cu UPS-ul" `
|
|
-Body ((Format-Reading -Reading $r -Config $Config) + @"
|
|
|
|
Windows nu mai vede bateria UPS-ului (BatteryFlag=$($r.BatteryFlag)).
|
|
Cat timp e asa, NU exista nicio protectie: nici oprirea automata a scriptului,
|
|
nici plasa de 20% a Windows-ului nu se pot declansa.
|
|
|
|
De verificat, in ordine: cablul USB, portul, alimentarea UPS-ului, apoi
|
|
Device Manager -> Batteries -> "HID UPS Battery".
|
|
"@) | Out-Null
|
|
$State.commsState = 'lost'
|
|
$State.lastCommsAlert = $now.ToString('o')
|
|
}
|
|
}
|
|
return $State # fara comunicatie nu luam nicio decizie de oprire
|
|
}
|
|
|
|
if ($script:CommsMissCount -gt 0 -or $State.commsState -eq 'lost') {
|
|
if ($State.commsState -eq 'lost') {
|
|
Write-Log 'Comunicatia cu UPS-ul a revenit'
|
|
Send-UpsMail -Config $Config `
|
|
-Subject "[UPS] $env:COMPUTERNAME - comunicatia cu UPS-ul a revenit" `
|
|
-Body (Format-Reading -Reading $r -Config $Config) | Out-Null
|
|
}
|
|
$script:CommsMissCount = 0
|
|
$State.commsState = 'ok'
|
|
$State.lastCommsAlert = $null
|
|
}
|
|
if ($State.commsState -eq 'unknown') { $State.commsState = 'ok' }
|
|
|
|
# --- trecerea pe baterie / revenirea curentului -------------------------
|
|
if ($r.OnBattery) {
|
|
$script:OnBatteryCount++
|
|
|
|
if ($State.powerState -ne 'battery') {
|
|
$State.powerState = 'battery'
|
|
$State.onBatterySince = $now.ToString('o')
|
|
$State.shutdownTriggered = $false
|
|
Write-Log "PANA DE CURENT - trecut pe baterie ($($r.Percent)%)" -Level ALERT
|
|
Send-UpsMail -Config $Config `
|
|
-Subject "[UPS] PANA DE CURENT - $env:COMPUTERNAME a trecut pe baterie" `
|
|
-Body ((Format-Reading -Reading $r -Config $Config) + @"
|
|
|
|
Daca curentul nu revine, serverul va fi oprit curat la primul prag atins.
|
|
Urmeaza un email si atunci.
|
|
"@) | Out-Null
|
|
}
|
|
|
|
$since = [datetime]$State.onBatterySince
|
|
$minutes = (New-TimeSpan -Start $since -End $now).TotalMinutes
|
|
|
|
if (-not $State.shutdownTriggered -and $script:OnBatteryCount -ge $Config.confirmPolls) {
|
|
$reason = $null
|
|
if ($minutes -ge $Config.shutdownAfterMinutesOnBattery) {
|
|
$reason = "$([math]::Round($minutes)) minute pe baterie (prag: $($Config.shutdownAfterMinutesOnBattery))"
|
|
} elseif ($null -ne $r.Percent -and $r.Percent -le $Config.shutdownAtBatteryPercent) {
|
|
$reason = "incarcare $($r.Percent)% (prag: $($Config.shutdownAtBatteryPercent)%)"
|
|
} elseif ($Config.shutdownAtRuntimeSeconds -gt 0 -and $null -ne $r.RuntimeSec -and $r.RuntimeSec -le $Config.shutdownAtRuntimeSeconds) {
|
|
$reason = "autonomie estimata $([math]::Round($r.RuntimeSec / 60, 1)) min (prag: $([math]::Round($Config.shutdownAtRuntimeSeconds / 60, 1)) min)"
|
|
}
|
|
|
|
if ($reason) {
|
|
$State.shutdownTriggered = $true
|
|
Save-State -State $State -Path $Config.stateFile
|
|
Invoke-UpsShutdown -Config $Config -Reading $r -Reason $reason -DryRun:$DryRun
|
|
}
|
|
}
|
|
} else {
|
|
$script:OnBatteryCount = 0
|
|
|
|
if ($State.powerState -eq 'battery') {
|
|
$durata = if ($State.onBatterySince) {
|
|
'{0:N1} minute' -f (New-TimeSpan -Start ([datetime]$State.onBatterySince) -End $now).TotalMinutes
|
|
} else { 'necunoscuta' }
|
|
Write-Log "Curentul a revenit dupa $durata"
|
|
Send-UpsMail -Config $Config `
|
|
-Subject "[UPS] Curentul a revenit - $env:COMPUTERNAME" `
|
|
-Body ("Serverul a stat pe baterie $durata.`n`n" + (Format-Reading -Reading $r -Config $Config)) | Out-Null
|
|
}
|
|
$State.powerState = 'online'
|
|
$State.onBatterySince = $null
|
|
$State.shutdownTriggered = $false
|
|
}
|
|
|
|
return $State
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Punctul de intrare
|
|
# ---------------------------------------------------------------------------
|
|
|
|
$cfg = Get-Config -Path $ConfigPath
|
|
$script:LogDir = $cfg.logPath
|
|
$script:OnBatteryCount = 0
|
|
$script:CommsMissCount = 0
|
|
|
|
switch ($Mode) {
|
|
|
|
'Status' {
|
|
$r = Get-PowerReading
|
|
Write-Host (Format-Reading -Reading $r -Config $cfg)
|
|
Write-Host ''
|
|
$st = Get-State -Path $cfg.stateFile
|
|
Write-Host "Stare salvata : power=$($st.powerState) comms=$($st.commsState) onBatterySince=$($st.onBatterySince)"
|
|
try {
|
|
$task = Get-ScheduledTask -TaskName 'ROMFAST UPS Monitor' -ErrorAction Stop
|
|
$info = Get-ScheduledTaskInfo -TaskName 'ROMFAST UPS Monitor'
|
|
Write-Host "Task programat : $($task.State), ultima rulare $($info.LastRunTime), rezultat $($info.LastTaskResult)"
|
|
} catch {
|
|
Write-Host 'Task programat : NEINREGISTRAT'
|
|
}
|
|
$log = Join-Path $cfg.logPath ('ups-monitor-{0}.log' -f (Get-Date -Format 'yyyy-MM'))
|
|
if (Test-Path -LiteralPath $log) {
|
|
Write-Host ''
|
|
Write-Host "Ultimele linii din $log :"
|
|
Get-Content -LiteralPath $log -Tail 15 | ForEach-Object { " $_" }
|
|
}
|
|
}
|
|
|
|
'TestMail' {
|
|
$r = Get-PowerReading
|
|
$ok = Send-UpsMail -Config $cfg `
|
|
-Subject "[UPS] Test - monitorizare $env:COMPUTERNAME" `
|
|
-Body ("Email de test din ups-monitor.ps1. Daca il primesti, alertarea functioneaza.`n`n" + (Format-Reading -Reading $r -Config $cfg))
|
|
if (-not $ok) { exit 1 }
|
|
}
|
|
|
|
'TestShutdown' {
|
|
$r = Get-PowerReading
|
|
Invoke-UpsShutdown -Config $cfg -Reading $r -Reason 'TEST (dry-run, nu se opreste nimic)' -DryRun
|
|
}
|
|
|
|
'Once' {
|
|
$st = Get-State -Path $cfg.stateFile
|
|
$st = Invoke-Cycle -Config $cfg -State $st
|
|
Save-State -State $st -Path $cfg.stateFile
|
|
Write-Host (Format-Reading -Reading (Get-PowerReading) -Config $cfg)
|
|
}
|
|
|
|
'Run' {
|
|
Write-Log "Pornit (interval $($cfg.pollSeconds)s, praguri: $($cfg.shutdownAfterMinutesOnBattery) min / $($cfg.shutdownAtBatteryPercent)%)"
|
|
$st = Get-State -Path $cfg.stateFile
|
|
$disableMarker = Join-Path (Split-Path -Parent $ConfigPath) 'DEZACTIVAT'
|
|
$wasDisabled = $false
|
|
|
|
while ($true) {
|
|
try {
|
|
if ((Test-Path -LiteralPath $disableMarker) -or -not $cfg.enabled) {
|
|
if (-not $wasDisabled) {
|
|
Write-Log 'Monitorizare DEZACTIVATA (marcaj DEZACTIVAT sau enabled=false) - doar astept' -Level WARN
|
|
$wasDisabled = $true
|
|
}
|
|
} else {
|
|
if ($wasDisabled) {
|
|
Write-Log 'Monitorizare reactivata'
|
|
$wasDisabled = $false
|
|
}
|
|
$st = Invoke-Cycle -Config $cfg -State $st
|
|
Save-State -State $st -Path $cfg.stateFile
|
|
}
|
|
} catch {
|
|
Write-Log "Eroare in ciclu: $($_.Exception.Message)" -Level ERROR
|
|
}
|
|
Start-Sleep -Seconds $cfg.pollSeconds
|
|
}
|
|
}
|
|
}
|