Lantul de protectie are doua jumatati. Prima - "UPS-ul chiar raporteaza trecerea pe baterie?" - e deja dovedita de perechile de evenimente Kernel-Power 105 de la panele reale din 2026. A doua - decizie, alerta, oprirea curata a Oracle - nu avea cum sa fie exercitata decat asteptand o pana. Acum monitorul accepta un SIMULARE.json care suprascrie citirea de alimentare. Doua garzi, ca un fisier uitat sa nu opreasca serverul pe date inventate: "expira" e obligatoriu si e sters automat la depasire, iar oprirea e in gol daca nu se cere explicit "dryRun": false. Cat e activa, fiecare ciclu scrie WARN in log si -Mode Status o afiseaza. test-battery.ps1 conduce simularea si urmareste logul; sterge fisierul si daca e intrerupt. Cu -ConfirmReal 'DA-OPRESTE-SERVERUL' devine repetitie reala pentru o fereastra de mentenanta - singurul mod de a masura cat dureaza shutdown immediate pe baza de productie, informatie de care depinde alegerea pragurilor. Validat in gol pe 27.08.2026: detectie la 15:36:09 + email de pana, apoi exact trei cicluri de confirmare, declansare pe pragul de 35% la 15:36:40 si secventa parcursa integral. Oracle a ramas Running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RRyaDj39hPQ89SZS6URRpS
680 lines
26 KiB
PowerShell
680 lines
26 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
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Simulare - testarea ramurii "pe baterie" fara sa scoti UPS-ul din priza
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# Cat timp exista SIMULARE.json langa config.json, citirea reala e suprascrisa
|
|
# cu valorile din el. Doua garzi, ca un fisier uitat sa nu opreasca serverul pe
|
|
# date inventate:
|
|
# - "expira" e OBLIGATORIU; fara el fisierul e ignorat, iar la depasire e sters;
|
|
# - oprirea e in gol (dry-run) daca nu se cere explicit "dryRun": false.
|
|
|
|
function Get-Simulation {
|
|
param([string]$Path)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) { return $null }
|
|
|
|
try {
|
|
$s = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
} catch {
|
|
Write-Log "SIMULARE.json nu se poate citi, il ignor: $($_.Exception.Message)" -Level WARN
|
|
return $null
|
|
}
|
|
|
|
if (-not ($s.PSObject.Properties.Name -contains 'expira') -or -not $s.expira) {
|
|
Write-Log 'SIMULARE.json nu are camp "expira" - ignorat, ca sa nu ramana activ la nesfarsit' -Level WARN
|
|
return $null
|
|
}
|
|
|
|
if ((Get-Date) -gt [datetime]$s.expira) {
|
|
Write-Log 'SIMULARE.json a expirat - il sterg si revin la citirea reala' -Level WARN
|
|
Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
|
return $null
|
|
}
|
|
|
|
return $s
|
|
}
|
|
|
|
function Merge-Simulation {
|
|
param([object]$Reading, [object]$Sim)
|
|
|
|
$o = $Reading.PSObject.Copy()
|
|
$map = @{
|
|
onBattery = 'OnBattery'
|
|
percent = 'Percent'
|
|
runtimeSec = 'RuntimeSec'
|
|
upsPresent = 'UpsPresent'
|
|
}
|
|
foreach ($k in $map.Keys) {
|
|
if ($Sim.PSObject.Properties.Name -contains $k) {
|
|
$o.($map[$k]) = $Sim.$k
|
|
}
|
|
}
|
|
$o.AcKnown = $true
|
|
return $o
|
|
}
|
|
|
|
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
|
|
|
|
# --- simulare (daca e activa, suprascrie citirea reala) -----------------
|
|
$sim = Get-Simulation -Path $script:SimPath
|
|
if ($sim) {
|
|
$r = Merge-Simulation -Reading $r -Sim $sim
|
|
$simDryRun = $true
|
|
if ($sim.PSObject.Properties.Name -contains 'dryRun') { $simDryRun = [bool]$sim.dryRun }
|
|
if ($simDryRun) { $DryRun = $true }
|
|
Write-Log ("SIMULARE ACTIVA pana la {0} (dryRun={1}): onBattery={2} percent={3} upsPresent={4}" -f `
|
|
$sim.expira, $simDryRun, $r.OnBattery, $r.Percent, $r.UpsPresent) -Level WARN
|
|
}
|
|
|
|
# --- 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:SimPath = Join-Path (Split-Path -Parent $ConfigPath) 'SIMULARE.json'
|
|
$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)"
|
|
if (Test-Path -LiteralPath $script:SimPath) {
|
|
Write-Host "SIMULARE : ACTIVA -> $script:SimPath"
|
|
Get-Content -LiteralPath $script:SimPath -Raw | ForEach-Object { " $_" }
|
|
}
|
|
if (Test-Path -LiteralPath (Join-Path (Split-Path -Parent $ConfigPath) 'DEZACTIVAT')) {
|
|
Write-Host 'AUTOMATISM : DEZARMAT (exista fisierul DEZACTIVAT)'
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|