Files
ROMFASTSQL/scripts/ups-monitor/ups-monitor.ps1
Marius 03d304540a fix(ups): -Mode Status nu mai raporteaza NEINREGISTRAT cand interogarea esueaza
Vazut pe 27.08.2026 la 16:27: Status a afisat "Task programat: NEINREGISTRAT"
desi taskul rula (State=Running, proces ups-monitor.ps1 -Mode Run viu, PID
3308). Trei rulari imediat urmatoare au raportat corect Running - deci o
eroare tranzitorie a Task Scheduler-ului, prezentata drept certitudine.

Cauza: catch-ul trata la fel "nu exista" si "n-am putut intreba", si alegea
mesajul cel mai alarmant. Intr-o pana reala ar trimite pe cineva sa reinstaleze
un monitor care de fapt merge.

Acum: absenta taskului se stabileste prin $null -eq $task, nu prin exceptie;
o interogare esuata spune NEDETERMINAT si arata eroarea; iar daca doar
Get-ScheduledTaskInfo cade, se afiseaza starea fara ultima rulare.

Verificat cu tokenizer-ul (0 erori) si pe server dupa deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRyaDj39hPQ89SZS6URRpS
2026-08-27 16:29:54 +03:00

693 lines
27 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)'
}
# "nu exista" si "n-am putut intreba" sunt lucruri diferite. Task Scheduler
# refuza ocazional interogarea (vazut pe 27.08.2026: un NEINREGISTRAT intre
# doua rulari care raportau corect Running), iar mesajul asta in mijlocul unei
# pene ar trimite pe cineva sa reinstaleze un monitor care de fapt merge.
$numeTask = 'ROMFAST UPS Monitor'
try {
$task = Get-ScheduledTask -TaskName $numeTask -ErrorAction SilentlyContinue
if ($null -eq $task) {
Write-Host 'Task programat : NEINREGISTRAT'
} else {
$info = Get-ScheduledTaskInfo -TaskName $numeTask -ErrorAction SilentlyContinue
if ($null -eq $info) {
Write-Host "Task programat : $($task.State)"
} else {
Write-Host "Task programat : $($task.State), ultima rulare $($info.LastRunTime), rezultat $($info.LastTaskResult)"
}
}
} catch {
Write-Host "Task programat : NEDETERMINAT - interogarea a esuat ($($_.Exception.Message))"
}
$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
}
}
}