ALL ARTICLES

KDE Connect'i Windows PC'm İçin Uzaktan Kumandaya Dönüştürdüm

KDE Connect, Tailscale, PowerShell ve Görev Zamanlayıcı'yı birleştirerek Windows PC'yi her yerden uzaktan kontrol etmek, işlemleri doğrulamak ve sistem durumunu izlemek için kurduğum sistem.

11 MIN READBY CEMİL İLKİM TEKE
Android telefondan Tailscale üzerinden KDE Connect kullanarak Windows PC'yi uzaktan kontrol etmeyi gösteren flat art illüstrasyon.

KDE Connect'i Windows PC'm İçin Uzaktan Kumandaya Dönüştürdüm

Bu kurulumun tamamı tek bir düşünceyle başladı:

Bir dakika. Bilgisayarı kilitledim mi?

Evden ya da ofisten çıktıktan sonra bilgisayarın ekranını açık bırakmış olabileceğin bir anda aklına geldiyse, bu sorunun ne kadar sinir bozucu olduğunu biliyorsundur.

KDE Connect zaten kurulu olduğu için ilk fikrim basitti: uzaktan kilitleme komutu eklerim, konu kapanır.

Çalıştı.

Sonra komutun gerçekten bilgisayara ulaşıp ulaşmadığını anlayamadığımı fark ettim.

Bu yüzden doğrulama bildirimi ekledim.

Ardından telefonum 5G'deyken ve bilgisayar tamamen farklı bir ağdayken de aynı sistemi kullanabilmek için Tailscale'i ekledim.

Sonrasında yeniden başlatma, kapatma, durum raporları, pil uyarıları, Wi-Fi takibi, disk uyarıları ve CPU/RAM kontrolleri geldi.

Küçük bir kolaylık fikri, zamanla Windows bilgisayarım için hafif bir uzaktan kontrol ve izleme sistemine dönüştü.

Bu yazı kurulumun eksiksiz sürümü. Normal bir KDE Connect kurulumundan başlayıp yalnızca bu rehberi takip ederek bütün sistemi kurabilmelisin.

Ne kuruyoruz?#

Kurulum tamamlandığında sistem şunları yapabiliyor:

  • bilgisayarı uzaktan kilitlemek
  • Windows gerçekten kilit durumuna geçtiğinde bunu telefona doğrulamak
  • bilgisayarın kilidi açıldığında bildirim göndermek
  • 30 saniyelik güvenlik süresiyle bilgisayarı yeniden başlatmak veya kapatmak
  • bekleyen yeniden başlatma veya kapatma işlemini iptal etmek
  • telefona hızlı bir PC durum raporu göndermek
  • kullanıcı oturumu açıldığında bilgisayarın çevrimiçi olduğunu bildirmek
  • uzaktan verilen restart veya shutdown komutunu normal oturum açılışından ayırt etmek
  • şarj cihazı bağlandığında veya çıkarıldığında bildirim göndermek
  • pil yüzde 20'nin veya yüzde 10'un altına düştüğünde uyarı vermek
  • Wi-Fi kopmasını ve geri gelmesini takip etmek
  • düşük disk alanı için uyarı vermek
  • uzun süren yüksek RAM veya CPU kullanımını bildirmek
  • telefon erişilemiyorken bildirimleri kuyruğa alıp daha sonra göndermek

Kullandığım yapı oldukça küçük:

  • KDE Connect uzaktan komutları ve telefon bildirimlerini yönetiyor
  • Tailscale telefon ile PC farklı ağlardayken özel bağlantıyı sağlıyor
  • PowerShell Windows tarafındaki otomasyonu yürütüyor
  • Görev Zamanlayıcı Windows olaylarını yakalıyor ve izleme ajanını çalıştırıyor

Genel mimari kabaca şöyle:

Android telefon
      |
      | 5G / Wi-Fi
      |
  Tailscale
      |
  Windows PC
      |
 KDE Connect
      |
PowerShell + Görev Zamanlayıcı

Buradaki önemli tasarım tercihi, KDE Connect'i doğrudan internete açmamak.


1. KDE Connect'i kur ve test et#

KDE Connect'i hem Windows bilgisayara hem Android telefona kur.

İlk eşleştirmeyi iki cihaz aynı yerel ağdayken normal şekilde yap.

Başka bir şey kurcalamadan önce KDE Connect'in temel özelliklerinin çalıştığından emin ol. Ping, pano paylaşımı, dosya aktarımı ve Run Commands iyi testlerdir.

Windows'ta PowerShell aç ve şunu çalıştır:

kdeconnect-cli -a --id-only

Şuna benzer bir cihaz kimliği görmelisin:

YOUR_PHONE_DEVICE_ID

Bu değeri sakla. Bilgisayarın daha sonra telefona bildirim gönderebilmesi için kullanacağız.

Windows'taki garip Qt uyarısı#

Benim Windows kurulumumda kdeconnect-cli şu mesajı da basıyordu:

QEventDispatcherWin32::wakeUp: Failed to post a message (Invalid window handle.)

İlk gördüğümde komutun başarısız olduğunu düşündüm.

Değilmiş.

Şu komut buna rağmen telefonuma bildirimi başarıyla gönderdi:

kdeconnect-cli -d YOUR_PHONE_DEVICE_ID --ping-msg "Test notification"

Terminal şikayet etti, bildirim geldi.

Aynı Qt uyarısını görürsen doğrudan uyarının peşine düşmeden önce gerçek sonucu test et. Aşağıdaki scriptlerde bu stderr gürültüsünü özellikle bastırıyorum.


2. Farklı ağlardan erişim için Tailscale ekle#

KDE Connect iki cihaz aynı ağdayken zaten kullanışlı. Buradaki asıl amaç telefon ve PC birbirinden tamamen farklı ağlardayken de sistemi kullanabilmek.

Tailscale'i iki cihaza da kur ve aynı tailnet hesabıyla giriş yap.

Bilgisayar genellikle şuna benzeyen bir Tailscale IP'si alır:

100.x.x.x

Android'de KDE Connect'i aç. Devices ekranındaki üç nokta menüsünden Add devices by IP seçeneğini aç ve bilgisayarın Tailscale IP'sini gir.

Gerçekten uzaktan çalıştığını test et#

Bunu iki cihaz aynı Wi-Fi'dayken test etme. O durumda Tailscale'in işe karışıp karışmadığını anlayamazsın.

Şöyle test et:

  1. Windows bilgisayar Wi-Fi'ya bağlı kalsın.
  2. Telefonda Wi-Fi'yi kapat.
  3. Telefonu 4G veya 5G'ye geçir.
  4. Tailscale açık kalsın.
  5. KDE Connect'i aç ve bilgisayarın erişilebilir olup olmadığını kontrol et.
  6. Ping gönder veya zararsız bir komut çalıştır.

Bu çalışıyorsa KDE Connect artık aynı fiziksel ağla sınırlı değil.

Ben Tailscale'i exit node kullanmadan açık bırakıyorum. Bu durumda normal internet trafiğim hâlâ telefonun kendi Wi-Fi veya mobil bağlantısından çıkıyor. Sadece tailnet içindeki cihazlara giden trafik Tailscale üzerinden gidiyor.

Kavramsal olarak:

Telefon -> normal web sitesi

normal internet bağlantısını kullanıyor.

Şu trafik ise:

Telefon -> Tailscale -> Windows PC

özel Tailscale bağlantısından geçiyor.

Android'de başka bir VPN de kullanıyorsan telefon aynı anda yalnızca tek VPN arayüzünü aktif tutabilir. Benim kullanımımda Proton VPN'i açınca Tailscale kapanıyor. Proton ile işim bitince Tailscale'i yeniden açıyorum ve KDE Connect kaldığı yerden devam ediyor.

Bu kullanım için benim açımdan yeterli.

Tailscale üzerinden KDE Connect erişilemiyorsa#

Önce telefonun Tailscale içinde bilgisayarı görebildiğini doğrula.

Tailscale bağlantısı çalışıyor fakat KDE Connect hâlâ bağlanamıyorsa Windows Defender Firewall'u kontrol et ve KDE Connect'e izin verildiğinden emin ol. KDE Connect dinamik TCP ve UDP portları olarak 1714-1764 aralığını kullanır.

Bunu router üzerinden bu portları internete açarak çözmeye çalışma. Tailscale kullanmamızın amacı KDE Connect'i doğrudan herkese açık internete çıkarmamak.


3. Script klasörünü oluştur#

Bu kurulumla ilgili her şeyi şu klasörde tutuyorum:

C:\Scripts\KDEConnectRemote

PowerShell açıp klasörü oluştur:

New-Item -ItemType Directory -Force C:\Scripts\KDEConnectRemote

Aşağıdaki bütün dosyalar bu klasörde olacak.


4. Yapılandırma dosyasını oluştur#

Şu dosyayı oluştur:

C:\Scripts\KDEConnectRemote\config.json

İçeriği:

{
  "DeviceId": "YOUR_PHONE_DEVICE_ID",
  "BatteryLowPercent": 20,
  "BatteryCriticalPercent": 10,
  "DiskLowGB": 15,
  "RamHighPercent": 90,
  "CpuHighPercent": 95,
  "SustainedMinutes": 5
}

Şuradaki:

YOUR_PHONE_DEVICE_ID

değerini şu komuttan aldığın kendi cihaz kimliğinle değiştir:

kdeconnect-cli -a --id-only

Diğer değerler izleme eşikleridir. Daha sonra monitor scriptine dokunmadan buradan değiştirebilirsin.


5. Ortak bildirim scriptini oluştur#

Bu dosya bütün sistemin ortak bildirim katmanı olacak.

Şunu oluştur:

C:\Scripts\KDEConnectRemote\Notify-KDE.ps1

İçeriği:

param(
    [Parameter(Mandatory = $true, Position = 0)]
    [string]$Message,

    [int]$WaitSeconds = 0,

    [switch]$NoQueue
)

$ErrorActionPreference = "Continue"

$Root = "C:\Scripts\KDEConnectRemote"
$Config = Get-Content (Join-Path $Root "config.json") -Raw | ConvertFrom-Json
$QueueFile = Join-Path $Root "notification-queue.ndjson"

$Cli = (Get-Command kdeconnect-cli.exe -ErrorAction SilentlyContinue).Source

if (-not $Cli) {
    $Fallback = "C:\Program Files\KDE Connect\bin\kdeconnect-cli.exe"

    if (Test-Path $Fallback) {
        $Cli = $Fallback
    }
}

if (-not $Cli) {
    exit 2
}

function Test-PhoneReachable {
    try {
        $Ids = @(
            & $Cli -a --id-only 2>$null |
            ForEach-Object { $_.Trim() }
        )

        return $Ids -contains [string]$Config.DeviceId
    }
    catch {
        return $false
    }
}

$Deadline = (Get-Date).AddSeconds(
    [Math]::Max(0, $WaitSeconds)
)

do {
    if (Test-PhoneReachable) {
        & $Cli -d $Config.DeviceId --ping-msg $Message 2>$null | Out-Null
        exit 0
    }

    if ((Get-Date) -ge $Deadline) {
        break
    }

    Start-Sleep -Seconds 2
}
while ($true)

if (-not $NoQueue) {
    $Item = [ordered]@{
        Time    = (Get-Date).ToString("o")
        Message = $Message
    } | ConvertTo-Json -Compress

    Add-Content -LiteralPath $QueueFile -Value $Item -Encoding UTF8
}

exit 1

Buradaki güzel kısım kuyruk mantığı.

Telefon geçici olarak erişilemiyorsa, örneğin Tailscale kapalıysa veya bilgisayarın ağı gitmişse, mesaj kaybolmak yerine yerel kuyruğa yazılıyor.

Bildirim scriptini test et#

Şunu çalıştır:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\KDEConnectRemote\Notify-KDE.ps1 "Test notification"

Telefon şunu almalı:

Test notification

Bu çalışmadan devam etme.


6. Bildirim kuyruğunu boşaltan scripti oluştur#

Şunu oluştur:

C:\Scripts\KDEConnectRemote\Flush-KDEQueue.ps1

İçeriği:

$ErrorActionPreference = "Continue"

$Root = "C:\Scripts\KDEConnectRemote"
$Config = Get-Content (Join-Path $Root "config.json") -Raw | ConvertFrom-Json
$QueueFile = Join-Path $Root "notification-queue.ndjson"

if (-not (Test-Path $QueueFile)) {
    exit 0
}

$Cli = (Get-Command kdeconnect-cli.exe -ErrorAction SilentlyContinue).Source

if (-not $Cli) {
    $Fallback = "C:\Program Files\KDE Connect\bin\kdeconnect-cli.exe"

    if (Test-Path $Fallback) {
        $Cli = $Fallback
    }
}

if (-not $Cli) {
    exit 2
}

try {
    $Ids = @(
        & $Cli -a --id-only 2>$null |
        ForEach-Object { $_.Trim() }
    )
}
catch {
    exit 1
}

if ($Ids -notcontains [string]$Config.DeviceId) {
    exit 1
}

$Lines = @(
    Get-Content -LiteralPath $QueueFile -ErrorAction SilentlyContinue
)

if ($Lines.Count -eq 0) {
    Remove-Item $QueueFile -Force -ErrorAction SilentlyContinue
    exit 0
}

Remove-Item $QueueFile -Force -ErrorAction SilentlyContinue

foreach ($Line in $Lines) {
    if ([string]::IsNullOrWhiteSpace($Line)) {
        continue
    }

    try {
        $Item = $Line | ConvertFrom-Json
        $Time = [datetime]::Parse([string]$Item.Time)
        $Age = (Get-Date) - $Time

        if ($Age.TotalMinutes -ge 2) {
            $Message = "$($Item.Message) (at $($Time.ToString('HH:mm')))"
        }
        else {
            $Message = [string]$Item.Message
        }

        & $Cli -d $Config.DeviceId --ping-msg $Message 2>$null | Out-Null

        Start-Sleep -Milliseconds 300
    }
    catch {}
}

Bir bildirim kuyrukta iki dakikadan fazla beklerse, teslim edildiğinde oluştuğu saat de mesaja eklenir.

Bu özellikle ağ kesintilerinde kullanışlı.


7. Kilit ve kilit açma bildirim scriptini oluştur#

Şunu oluştur:

C:\Scripts\KDEConnectRemote\Event-Notify.ps1

İçeriği:

param(
    [Parameter(Mandatory = $true)]
    [ValidateSet("Locked", "Unlocked")]
    [string]$Event
)

$Root = "C:\Scripts\KDEConnectRemote"

switch ($Event) {
    "Locked" {
        $Message = "PC locked"
    }

    "Unlocked" {
        $Message = "PC unlocked"
    }
}

& (Join-Path $Root "Notify-KDE.ps1") $Message | Out-Null

Bu script bilgisayarın ne zaman kilitlendiğine kendisi karar vermiyor. O olayı Windows'tan Görev Zamanlayıcı ile alacağız.

Bu önemli çünkü PC locked bildirimi, Run Command'a bastığımız için değil, Windows'un gerçek workstation lock olayı gerçekleştiği için gönderilecek.


8. Uzaktan kontrol scriptini oluştur#

Şunu oluştur:

C:\Scripts\KDEConnectRemote\Remote-Control.ps1

İçeriği:

param(
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateSet(
        "Lock",
        "Shutdown30",
        "Restart30",
        "CancelPower",
        "Status"
    )]
    [string]$Action
)

$Root = "C:\Scripts\KDEConnectRemote"
$NotifyScript = Join-Path $Root "Notify-KDE.ps1"
$Marker = Join-Path $Root "power-action.json"

function Send-Notice {
    param([string]$Message)

    & $NotifyScript $Message | Out-Null
}

function Write-PowerMarker {
    param([string]$Type)

    [ordered]@{
        Action = $Type
        Time   = (Get-Date).ToString("o")
    } |
        ConvertTo-Json |
        Set-Content -LiteralPath $Marker -Encoding UTF8
}

switch ($Action) {
    "Lock" {
        rundll32.exe user32.dll,LockWorkStation
    }

    "Shutdown30" {
        shutdown.exe /s /t 30

        if ($LASTEXITCODE -eq 0) {
            Write-PowerMarker "shutdown"
            Send-Notice "PC will shut down in 30 seconds"
        }
        else {
            Send-Notice "Shutdown request failed"
        }
    }

    "Restart30" {
        shutdown.exe /r /t 30

        if ($LASTEXITCODE -eq 0) {
            Write-PowerMarker "restart"
            Send-Notice "PC will restart in 30 seconds"
        }
        else {
            Send-Notice "Restart request failed"
        }
    }

    "CancelPower" {
        shutdown.exe /a

        if ($LASTEXITCODE -eq 0) {
            Remove-Item -LiteralPath $Marker -Force -ErrorAction SilentlyContinue
            Send-Notice "Pending shutdown/restart cancelled"
        }
        else {
            Send-Notice "No pending shutdown/restart to cancel"
        }
    }

    "Status" {
        $Parts = New-Object System.Collections.Generic.List[string]

        try {
            $Battery = Get-CimInstance Win32_Battery -ErrorAction Stop |
                Select-Object -First 1

            if ($Battery) {
                $Parts.Add(
                    "Battery $($Battery.EstimatedChargeRemaining)%"
                )
            }
        }
        catch {}

        try {
            $OS = Get-CimInstance Win32_OperatingSystem

            $RamUsed = [math]::Round(
                (
                    (
                        $OS.TotalVisibleMemorySize -
                        $OS.FreePhysicalMemory
                    ) /
                    $OS.TotalVisibleMemorySize
                ) * 100
            )

            $Parts.Add("RAM $RamUsed%")

            $Uptime = (Get-Date) - $OS.LastBootUpTime

            if ($Uptime.TotalDays -ge 1) {
                $Parts.Add(
                    "Uptime $([math]::Floor($Uptime.TotalDays))d $($Uptime.Hours)h"
                )
            }
            else {
                $Parts.Add(
                    "Uptime $($Uptime.Hours)h $($Uptime.Minutes)m"
                )
            }
        }
        catch {}

        try {
            $Drive = Get-PSDrive C
            $FreeGB = [math]::Round($Drive.Free / 1GB, 1)

            $Parts.Add("C: $FreeGB GB free")
        }
        catch {}

        try {
            $WiFi = Get-NetAdapter -ErrorAction SilentlyContinue |
                Where-Object {
                    $_.Name -match "Wi-Fi|WLAN" -or
                    $_.InterfaceDescription -match "Wireless|802\.11"
                } |
                Select-Object -First 1

            if ($WiFi) {
                $Parts.Add("Wi-Fi $($WiFi.Status)")
            }
        }
        catch {}

        if ($Parts.Count -eq 0) {
            Send-Notice "PC is online"
        }
        else {
            Send-Notice (
                "PC online | " + ($Parts -join " | ")
            )
        }
    }
}

Shutdown ve restart komutları özellikle 30 saniyelik gecikme kullanıyor.

Yanlışlıkla bastığını fark edip iptal etmek için zaman kazandırıyor.

Bir not: sayaç sıfıra ulaştığında Windows uygulamaları kapatabilir. Bu sistemi kaydedilmemiş dosyaları koruyan güvenli bir shutdown mekanizması gibi düşünme.


9. İzleme scriptini oluştur#

Şunu oluştur:

C:\Scripts\KDEConnectRemote\Monitor-Once.ps1

İçeriği:

$Root = "C:\Scripts\KDEConnectRemote"
$Config = Get-Content (Join-Path $Root "config.json") -Raw | ConvertFrom-Json
$NotifyScript = Join-Path $Root "Notify-KDE.ps1"
$StateFile = Join-Path $Root "monitor-state.json"

function Send-Notice {
    param([string]$Message)

    & $NotifyScript $Message | Out-Null
}

$State = @{}

if (Test-Path $StateFile) {
    try {
        $Object = Get-Content $StateFile -Raw | ConvertFrom-Json

        foreach ($Property in $Object.PSObject.Properties) {
            $State[$Property.Name] = $Property.Value
        }
    }
    catch {}
}

function Get-State {
    param(
        [string]$Key,
        $Default
    )

    if ($State.ContainsKey($Key)) {
        return $State[$Key]
    }

    return $Default
}

& (Join-Path $Root "Flush-KDEQueue.ps1") | Out-Null

try {
    $BatteryDevice = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue |
        Select-Object -First 1

    if ($BatteryDevice) {
        Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue

        $Power = [System.Windows.Forms.SystemInformation]::PowerStatus
        $PowerLine = $Power.PowerLineStatus.ToString()
        $ACKnown = $PowerLine -ne "Unknown"
        $ACOnline = $PowerLine -eq "Online"
        $BatteryPercent = [int]$BatteryDevice.EstimatedChargeRemaining

        if ($ACKnown) {
            $PreviousAC = Get-State "acOnline" $ACOnline

            if ($PreviousAC -eq $true -and $ACOnline -eq $false) {
                Send-Notice "Charger disconnected"
            }
            elseif ($PreviousAC -eq $false -and $ACOnline -eq $true) {
                Send-Notice "Charger connected"
            }

            $State["acOnline"] = $ACOnline
        }

        $BatteryLow = [bool](
            Get-State "batteryLow" $false
        )

        $BatteryCritical = [bool](
            Get-State "batteryCritical" $false
        )

        if (
            -not $ACOnline -and
            $BatteryPercent -lt [int]$Config.BatteryCriticalPercent -and
            -not $BatteryCritical
        ) {
            Send-Notice "Battery critical: $BatteryPercent% remaining"

            $State["batteryCritical"] = $true
            $State["batteryLow"] = $true
        }
        elseif (
            -not $ACOnline -and
            $BatteryPercent -lt [int]$Config.BatteryLowPercent -and
            -not $BatteryLow
        ) {
            Send-Notice "Battery low: $BatteryPercent% remaining"

            $State["batteryLow"] = $true
        }

        if (
            $ACOnline -or
            $BatteryPercent -ge ([int]$Config.BatteryLowPercent + 5)
        ) {
            $State["batteryLow"] = $false
        }

        if (
            $ACOnline -or
            $BatteryPercent -ge ([int]$Config.BatteryCriticalPercent + 5)
        ) {
            $State["batteryCritical"] = $false
        }
    }
}
catch {}

try {
    $WiFi = Get-NetAdapter -ErrorAction SilentlyContinue |
        Where-Object {
            $_.Name -match "Wi-Fi|WLAN" -or
            $_.InterfaceDescription -match "Wireless|802\.11"
        } |
        Select-Object -First 1

    if ($WiFi) {
        $WiFiUp = $WiFi.Status -eq "Up"
        $PreviousWiFi = Get-State "wifiUp" $WiFiUp

        if (
            $PreviousWiFi -eq $true -and
            $WiFiUp -eq $false
        ) {
            $State["wifiDownAt"] = (Get-Date).ToString("o")

            Send-Notice "Wi-Fi disconnected"
        }
        elseif (
            $PreviousWiFi -eq $false -and
            $WiFiUp -eq $true
        ) {
            $DisconnectedAt = Get-State "wifiDownAt" $null

            if ($DisconnectedAt) {
                try {
                    $DownDate = [datetime]::Parse(
                        [string]$DisconnectedAt
                    )

                    $Minutes = [math]::Max(
                        1,
                        [math]::Round(
                            ((Get-Date) - $DownDate).TotalMinutes
                        )
                    )

                    Send-Notice "Wi-Fi restored after $Minutes minute(s)"
                }
                catch {
                    Send-Notice "Wi-Fi restored"
                }
            }
            else {
                Send-Notice "Wi-Fi restored"
            }

            $State["wifiDownAt"] = $null
        }

        $State["wifiUp"] = $WiFiUp
    }
}
catch {}

try {
    $Drive = Get-PSDrive -Name C

    $FreeGB = [math]::Round(
        $Drive.Free / 1GB,
        1
    )

    $TotalGB = [math]::Round(
        ($Drive.Used + $Drive.Free) / 1GB,
        1
    )

    if ($TotalGB -gt 0) {
        $FreePercent = [math]::Round(
            ($FreeGB / $TotalGB) * 100
        )
    }
    else {
        $FreePercent = 100
    }

    $DiskLow = [bool](
        Get-State "diskLow" $false
    )

    if (
        (
            $FreeGB -lt [double]$Config.DiskLowGB -or
            $FreePercent -lt 10
        ) -and
        -not $DiskLow
    ) {
        Send-Notice "Low disk space: C: has $FreeGB GB free ($FreePercent%)"

        $State["diskLow"] = $true
    }
    elseif (
        $FreeGB -gt ([double]$Config.DiskLowGB + 5) -and
        $FreePercent -gt 12
    ) {
        $State["diskLow"] = $false
    }
}
catch {}

try {
    $OS = Get-CimInstance Win32_OperatingSystem

    $RamPercent = [math]::Round(
        (
            (
                $OS.TotalVisibleMemorySize -
                $OS.FreePhysicalMemory
            ) /
            $OS.TotalVisibleMemorySize
        ) * 100
    )

    $RamCount = [int](
        Get-State "ramHighCount" 0
    )

    $RamAlerted = [bool](
        Get-State "ramAlerted" $false
    )

    if ($RamPercent -ge [int]$Config.RamHighPercent) {
        $RamCount++

        if (
            $RamCount -ge [int]$Config.SustainedMinutes -and
            -not $RamAlerted
        ) {
            Send-Notice "High RAM usage: $RamPercent%"
            $RamAlerted = $true
        }
    }
    else {
        $RamCount = 0

        if (
            $RamPercent -lt ([int]$Config.RamHighPercent - 10)
        ) {
            $RamAlerted = $false
        }
    }

    $State["ramHighCount"] = $RamCount
    $State["ramAlerted"] = $RamAlerted
}
catch {}

try {
    $CpuAverage = (
        Get-CimInstance Win32_Processor |
        Measure-Object LoadPercentage -Average
    ).Average

    $CpuPercent = [math]::Round($CpuAverage)

    $CpuCount = [int](
        Get-State "cpuHighCount" 0
    )

    $CpuAlerted = [bool](
        Get-State "cpuAlerted" $false
    )

    if ($CpuPercent -ge [int]$Config.CpuHighPercent) {
        $CpuCount++

        if (
            $CpuCount -ge [int]$Config.SustainedMinutes -and
            -not $CpuAlerted
        ) {
            Send-Notice "High CPU usage: $CpuPercent%"
            $CpuAlerted = $true
        }
    }
    else {
        $CpuCount = 0

        if (
            $CpuPercent -lt ([int]$Config.CpuHighPercent - 15)
        ) {
            $CpuAlerted = $false
        }
    }

    $State["cpuHighCount"] = $CpuCount
    $State["cpuAlerted"] = $CpuAlerted
}
catch {}

$State |
    ConvertTo-Json -Depth 5 |
    Set-Content -LiteralPath $StateFile -Encoding UTF8

& (Join-Path $Root "Flush-KDEQueue.ps1") | Out-Null

Monitor önceki durumu hatırlıyor.

Bu yüzden pil yüzde 19'a düştüğünde her dakika tekrar Battery low bildirimi göndermez.

config.json içindeki varsayılan değerlerle:

  • pil yüzde 20'nin altına düşünce uyarı gelir
  • pil yüzde 10'un altına düşünce kritik uyarı gelir
  • C: sürücüsünde 15 GB'dan veya yüzde 10'dan az boş yer kalınca uyarı gelir
  • RAM yüzde 90'a ulaşınca izlenmeye başlanır
  • CPU yüzde 95'e ulaşınca izlenmeye başlanır
  • CPU ve RAM uyarı vermeden önce yaklaşık beş kontrol boyunca yüksek kalmalıdır

Monitor dakikada bir çalışacağı için SustainedMinutes: 5 yaklaşık beş dakikalık sürekli yüksek yük anlamına gelir.


10. Session Agent scriptini oluştur#

Her bir dakikalık sağlık kontrolü için ayrı bir Scheduled Task oluşturmak yerine tek bir küçük arka plan PowerShell süreci kullanıyorum.

Oturum açıldığında başlıyor, bilgisayarın çevrimiçi olduğunu bildiriyor ve ardından monitor scriptini dakikada bir çalıştırıyor.

Şunu oluştur:

C:\Scripts\KDEConnectRemote\Session-Agent.ps1

İçeriği:

$Root = "C:\Scripts\KDEConnectRemote"
$Marker = Join-Path $Root "power-action.json"
$NotifyScript = Join-Path $Root "Notify-KDE.ps1"

Start-Sleep -Seconds 8

$Message = "PC is online"

if (Test-Path $Marker) {
    try {
        $MarkerData = Get-Content $Marker -Raw | ConvertFrom-Json

        switch ([string]$MarkerData.Action) {
            "restart" {
                $Message = "PC restarted successfully"
            }

            "shutdown" {
                $Message = "PC started after shutdown"
            }
        }
    }
    catch {}

    Remove-Item -LiteralPath $Marker -Force -ErrorAction SilentlyContinue
}

& $NotifyScript $Message -WaitSeconds 60 | Out-Null

while ($true) {
    try {
        & (Join-Path $Root "Monitor-Once.ps1") | Out-Null
    }
    catch {}

    Start-Sleep -Seconds 60
}

Burada önemli bir sınır var.

Agent Windows login ekranına gelmeden önce değil, kullanıcı oturum açtığında başlıyor. KDE Connect de normalde kullanıcı masaüstü oturumunda çalıştığı için restart sonrası doğrulama bildirimi Windows'a yeniden giriş yapana kadar gelmeyebilir.

Benim kullanımım için bu sorun değil.


11. Terminal penceresi çakmasını engelle#

Görev Zamanlayıcı PowerShell'i doğrudan başlatırsa görev çalışırken ekranda çok kısa süreli bir terminal penceresi görünebilir.

Zararsızdır ama çirkin durur.

PowerShell scriptlerini görünmez çalıştırmak için küçük bir VBScript wrapper kullanıyorum.

Şunu oluştur:

C:\Scripts\KDEConnectRemote\RunHidden.vbs

İçeriği:

Option Explicit

Dim shell, mode, root, cmd, q

root = "C:\Scripts\KDEConnectRemote\"
q = Chr(34)

Set shell = CreateObject("WScript.Shell")

If WScript.Arguments.Count = 0 Then
    WScript.Quit 1
End If

mode = LCase(WScript.Arguments(0))

Select Case mode

    Case "locked"
        cmd = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " & _
              q & root & "Event-Notify.ps1" & q & " -Event Locked"

    Case "unlocked"
        cmd = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " & _
              q & root & "Event-Notify.ps1" & q & " -Event Unlocked"

    Case "agent"
        cmd = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " & _
              q & root & "Session-Agent.ps1" & q

    Case Else
        WScript.Quit 1

End Select

shell.Run cmd, 0, True

shell.Run içindeki ikinci argüman 0 olduğu için PowerShell penceresi görünmez.

Son True değeri wrapper'ın PowerShell işlemi bitene kadar beklemesini sağlar. Böylece Görev Zamanlayıcı görevin gerçekten çalışmaya devam ettiğini takip edebilir.


12. Görev Zamanlayıcı'yı yapılandır

Burası sistemin olay katmanı.

PowerShell ne yapılacağını biliyor, Görev Zamanlayıcı ise Windows'a bu işlemlerin ne zaman çalışacağını söylüyor.

Görev Zamanlayıcı'yı aç:

taskschd.msc

Create Basic Task yerine Create Task kullan.

Üç görev oluşturacağız:

  • KDE Remote - PC Locked
  • KDE Remote - PC Unlocked
  • KDE Remote - Session Agent

Görev 1: PC kilitlendi#

Yeni görev oluştur.

General#

Şunu kullan:

Name:
KDE Remote - PC Locked

Şunu seç:

Run only when user is logged on

Run with highest privileges gerekli değil.

Trigger#

Triggers -> New aç.

Şunu ayarla:

Begin the task:
On workstation lock

Kullanıcı seçme alanı görünüyorsa bütün kullanıcılar yerine kendi Windows hesabını seç.

Trigger'ın enabled olduğundan emin ol.

Action#

Actions -> New aç.

Şunu seç:

Start a program

Program:

C:\Windows\System32\wscript.exe

Arguments:

"C:\Scripts\KDEConnectRemote\RunHidden.vbs" locked

Conditions#

Şunu kapat:

Start the task only if the computer is on AC power

Bu özellikle laptopta önemli. Şarj cihazı çıkarıldı diye kilit doğrulamasının çalışmayı bırakmasını istemeyiz.

Settings#

Görev enabled kalsın.

Şu bölümde:

If the task is already running

şunu kullan:

Do not start a new instance

Görevi kaydet.

Windows gerçekten kilit durumuna geçtiğinde bu görev Event-Notify.ps1 scriptini çalıştırır ve telefona:

PC locked

gönderir.

Bizim için önemli doğrulama budur.

Görev 2: PC kilidi açıldı#

İkinci bir görev oluştur.

General#

Name:
KDE Remote - PC Unlocked

Yine şunu seç:

Run only when user is logged on

Trigger#

Şunu kullan:

On workstation unlock

Action#

Program:

C:\Windows\System32\wscript.exe

Arguments:

"C:\Scripts\KDEConnectRemote\RunHidden.vbs" unlocked

Conditions#

Şunu kapat:

Start the task only if the computer is on AC power

Settings#

Şunu kullan:

If the task is already running:
Do not start a new instance

Kaydet.

Artık Windows kilidi açıldığında telefona:

PC unlocked

gelir.

Görev 3: Session Agent#

Bu görev Windows'a giriş yaptığında uzun süre çalışan monitor ajanını başlatır.

Yeni bir görev oluştur.

General#

Şunu kullan:

Name:
KDE Remote - Session Agent

Şunu seç:

Run only when user is logged on

Trigger#

Şu trigger'ı oluştur:

At log on

Kendi Windows kullanıcı hesabını seç.

Action#

Program:

C:\Windows\System32\wscript.exe

Arguments:

"C:\Scripts\KDEConnectRemote\RunHidden.vbs" agent

Conditions#

İkisini de kapat:

Start the task only if the computer is on AC power

ve:

Stop if the computer switches to battery power

Yoksa Windows gerçekten komik bir şey yapabilir: laptop bataryaya geçtiği için batarya izleme sürecini durdurabilir.

Settings#

Şunu kullan:

If the task is already running:
Do not start a new instance

En önemlisi şu ayarı kapat:

Stop the task if it runs longer than:

Session Agent oturum boyunca açık kalmak için tasarlandı. Görev Zamanlayıcı'da varsayılan uzun süre sınırı açıksa, Windows birkaç gün sonra monitor'ı gereksiz yere kapatabilir.

Görevi kaydet.

Çıkış yapmadan Session Agent'ı başlat#

Sadece test etmek için Windows'tan çıkış yapmana gerek yok.

Task Scheduler Library içinde:

KDE Remote - Session Agent

görevini bul, sağ tıkla ve:

Run

de.

Birkaç saniye sonra telefona:

PC is online

gelmeli.

Kilit ve kilit açma görevlerini test et#

Şuna bas:

Win + L

Telefona:

PC locked

gelmeli.

Tekrar giriş yap.

Bu sefer:

PC unlocked

gelmeli.

Ekranda PowerShell veya terminal penceresi çakmamalı.

İki bildirim de geliyorsa Görev Zamanlayıcı tarafı doğru çalışıyor.


13. KDE Connect Run Commands komutlarını ekle#

Şimdi telefonda göreceğin asıl butonları ekleyelim.

Windows'ta KDE Connect'in Run Commands ayarını aç ve aşağıdaki komutları arayüzden tek tek oluştur.

KDE Connect'in iç config dosyasını doğrudan düzenlemek yerine arayüzü kullanmanı özellikle öneriyorum.

Ben bunu otomatikleştirmeyi denedim.

Bütün mevcut Run Commands komutlarım kayboldu.

Yedek sayesinde geri aldım ama mesajı aldım.

Lock PC#

Name:

Lock PC

Command:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\KDEConnectRemote\Remote-Control.ps1 Lock

Shut Down in 30s#

Name:

Shut Down in 30s

Command:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\KDEConnectRemote\Remote-Control.ps1 Shutdown30

Restart in 30s#

Name:

Restart in 30s

Command:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\KDEConnectRemote\Remote-Control.ps1 Restart30

Cancel Shutdown / Restart#

Name:

Cancel Shutdown / Restart

Command:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\KDEConnectRemote\Remote-Control.ps1 CancelPower

PC Status#

Name:

PC Status

Command:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\KDEConnectRemote\Remote-Control.ps1 Status

Benim kullandığım son komut listesi bu.

Sign out ve hibernation seçeneklerini de denedim ama pratikte kullanmadığım için listeden çıkardım.


14. Bütün komutları test et#

Önce en risksiz olanla başla.

PC Status#

Telefondan:

PC Status

komutuna bas.

Şuna benzer bir bildirim gelmeli:

PC online | Battery 72% | RAM 36% | Uptime 4h 17m | C: 186.4 GB free | Wi-Fi Up

Lock PC#

Şuna bas:

Lock PC

Bilgisayar kilitlenmeli.

Biraz sonra telefon şunu almalı:

PC locked

Bu bildirimin Run Command tarafından doğrudan gönderilmediğini unutma. Windows'un gerçek workstation lock trigger'ı tetiklendikten sonra geliyor.

Bütün olay zaten bu.

Shutdown ve iptal#

Şuna bas:

Shut Down in 30s

Telefon şunu almalı:

PC will shut down in 30 seconds

30 saniye dolmadan:

Cancel Shutdown / Restart

komutuna bas.

Şu bildirimi almalısın:

Pending shutdown/restart cancelled

Bekleyen bir işlem yoksa sistem bunun yerine:

No pending shutdown/restart to cancel

gönderir.

Restart#

Gerçek restart testine hazır olduğunda:

Restart in 30s

komutuna bas.

Önce:

PC will restart in 30 seconds

gelmeli.

Windows yeniden başladıktan ve tekrar oturum açtıktan sonra Session Agent restart öncesi bırakılan marker dosyasını okuyup:

PC restarted successfully

gönderir.

Aynı mekanizma uzaktan verilen shutdown komutundan sonraki girişte:

PC started after shutdown

gönderebilir.

Marker olmayan normal bir giriş ise:

PC is online

mesajını üretir.


15. Otomatik monitor hangi bildirimleri gönderiyor?#

Session Agent çalıştığında bilgisayar yaklaşık dakikada bir temel durumunu kontrol eder.

Bildirimleri bilerek kısa tuttum.

Şunları görebilirsin:

Charger disconnected
Charger connected

Battery low: 19% remaining
Battery critical: 9% remaining

Wi-Fi disconnected
Wi-Fi restored after 7 minute(s)

Low disk space: C: has 12.4 GB free (5%)

High RAM usage: 92%
High CPU usage: 97%

State dosyası gereksiz tekrarları engeller.

Örneğin pil yüzde 19'da kaldı diye her dakika Battery low mesajı gelmez.

CPU ve RAM de tek bir anlık sıçramada bildirim göndermez. Eşik değerlerinin üzerinde birkaç kontrol boyunca kalmaları gerekir.

Böylece proje derlerken telefon bilgisayar yanıyormuş gibi davranmaz.


16. Wi-Fi izlemenin komik problemi#

Şu istek kulağa çok mantıklı geliyor:

Wi-Fi kopunca bana bildirim gönder.

Tek bir problem var.

Wi-Fi bilgisayarın tek internet bağlantısıysa, bilgisayar Wi-Fi'nin koptuğunu anlayabilir.

Ama sana söyleyemez.

Çünkü Wi-Fi koptu.

Bildirim helper'ı mesajı yerel kuyruğa yazar.

Bağlantı geri geldiğinde kuyruk boşaltılır ve örneğin:

Wi-Fi disconnected (at 16:42)

ardından:

Wi-Fi restored after 7 minute(s)

görebilirsin.

Bilgisayar aynı anda Ethernet gibi başka çalışan bir bağlantıya sahipse disconnect bildirimi anında da gönderilebilir.

PowerShell ile fizik kanunlarını henüz çözemedik.


17. Monitor'ın gerçekten çalıştığını kontrol et#

Task Manager'ı aç ve Details sekmesine bak.

Session Agent için çalışan gizli PowerShell işlemini ve onu takip eden wscript.exe işlemini görebilirsin.

Çoğu zaman CPU kullanımının sıfıra çok yakın olması gerekir.

Şu dosyayı da kontrol edebilirsin:

C:\Scripts\KDEConnectRemote\monitor-state.json

Monitor çalıştıkça buradaki değerler güncellenir.

Görev Zamanlayıcı da görevler için şu bilgileri gösterir:

Last Run Time
Last Run Result

Lock ve unlock görevlerinde son çalışma zamanı, bilgisayarı son kilitlediğin veya açtığın zamanla eşleşmelidir.

Bazı Windows KDE Connect sürümlerinin bildirim başarıyla gönderildiği halde Qt Invalid window handle uyarısı ürettiğini tekrar hatırlatayım. Bu sistemde telefonun beklenen bildirimi alması, uçtan uca en anlamlı başarı testidir.


18. Performansa etkisi#

Bu sistem oldukça hafif.

Lock ve unlock görevleri yalnızca ilgili Windows olayı olduğunda çalışıyor. Uzaktan kontrol komutları yalnızca telefondan bastığında çalışıyor. Session Agent zamanının büyük kısmında uyuyor, yaklaşık dakikada bir uyanıp birkaç sistem değerini okuyor, küçük bir JSON state dosyasını güncelliyor ve tekrar uyuyor.

Modern bir bilgisayarda etkisinin hissedilir olmaması gerekir.

Rastgele açık bırakılmış tek bir tarayıcı sekmesi muhtemelen bu sistemin tamamından daha fazla iş yapıyordur.

Daha da az arka plan etkinliği istiyorsan Session-Agent.ps1 içindeki:

Start-Sleep -Seconds 60

satırını:

Start-Sleep -Seconds 120

veya:

Start-Sleep -Seconds 300

olarak değiştirebilirsin.

Sadece pil ve monitor uyarılarının da aynı oranda gecikeceğini unutma.


19. Güvenlik notları#

KDE Connect portlarını router üzerinden doğrudan internete açmıyorum.

Bilgisayara uzaktan erişimi sağlayan katman Tailscale.

Bu sayede KDE Connect doğrudan herkese açık internete yayınlanmak yerine özel tailnet içinde kalıyor.

Tailscale için kullandığın hesabı güçlü kimlik doğrulama ve mümkünse MFA veya passkey ile koru.

Power komutlarına da dikkat et.

Uzaktan shutdown hâlâ gerçek bir shutdown. 30 saniyelik süre yanlışlıkla bastığında iptal şansı verir ama sayaç sıfıra ulaştığında kaydedilmemiş dosyaları sihirli şekilde korumaz.

Ayrıca gerçekten kullanmadığım uzaktan komutları listede tutmamayı tercih ediyorum. Daha küçük bir komut listesi hem daha anlaşılır hem de yanlışlıkla basması daha zor.


20. Bu sistemi gerçekten kullanmamı sağlayan kısım#

Benim için hâlâ en değerli özellik ilk fikir.

Laptop ofiste duruyor.

Ben dışarıdayım.

Telefon 5G'de.

Sonra o düşünce geliyor:

Bilgisayarı kilitledim mi?

KDE Connect'i açıp:

Lock PC

butonuna basıyorum.

Komut şu yolu izliyor:

Telefon
  |
Tailscale
  |
KDE Connect
  |
Windows

Windows kilitleniyor.

Sonra doğrulama ters yönde geliyor:

Windows lock olayı
  |
Görev Zamanlayıcı
  |
Event-Notify.ps1
  |
Notify-KDE.ps1
  |
KDE Connect
  |
Tailscale
  |
Telefon

Telefon şunu gösteriyor:

PC locked

Bitti.

Tahmin yok.

Sadece lock ekranına bakmak için Remote Desktop açmak yok.

Yolun yarısından geri dönmek yok.

Bildirim bana bir butona bastığımı söylemiyor. Windows'un gerçekten kilitlendiğini söylüyor.

Bütün sistemi tutmaya devam etmemi sağlayan küçük ama önemli fark bu.

Sonuç#

KDE Connect zaten gerekli yapı taşlarının büyük kısmını sağlıyor.

Tailscale aynı ağda olma zorunluluğunu kaldırıyor.

PowerShell Windows otomasyonunu yapıyor.

Görev Zamanlayıcı sisteme olay tabanlı katmanı ekliyor.

Hepsini bir araya getirince Android telefon, Windows bilgisayar için şaşırtıcı derecede yetenekli bir uzaktan kumandaya ve hafif izleme paneline dönüşüyor. Üstelik bilgisayarı doğrudan herkese açık internete açmadan.

Her şey şu düşünceyle başladı:

Bilgisayarı kilitlemeyi unuttum.

Sonunda ortaya şu çıktı:

Uzaktan kontrol
+ doğrulama
+ izleme
+ özel ağ

Sırf eve giderken Win + L tuşuna basıp basmadığımı düşünmek istemediğim için.

Gayet makul miktarda mühendislik, tabii ki.

ILKIM.DEV logo

About The Author

Cemil İlkim Teke

Full-stack developer building practical web, mobile, backend, and AI-enabled products.

WORK WITH ME