Great idea. I worked out a php page, with a password, using Claude last night. It will open and close the garage, with some feedback from HA about the garage door status. Claude even added some UI elements to make it look nice.
php:
<?php
// ββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
define('PAGE_PASSWORD', 'supersecretpassword');
define('HA_URL', 'https://home-assistant.mydomain.net/');
define('HA_TOKEN', 'home-assistant-long-lived-token');
define('HA_WEBHOOK_ID', 'very-long-random-char-string');
define('HA_DOOR_ENTITY', 'cover.smart_garage_door_opener');
define('STATE_DELAY_SEC', 2);
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
$message = '';
$messageClass = '';
$doorState = '';
$showCloseButton = false;
$showCloseConfirm = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = isset($_POST['action']) ? $_POST['action'] : 'open';
// ββ CLOSE ACTION βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if ($action === 'close') {
$serviceUrl = HA_URL . '/api/services/cover/close_cover';
$ch = curl_init($serviceUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['entity_id' => HA_DOOR_ENTITY]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . HA_TOKEN,
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
$closeStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
sleep(STATE_DELAY_SEC);
$stateUrl = HA_URL . '/api/states/' . HA_DOOR_ENTITY;
$ch = curl_init($stateUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . HA_TOKEN,
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$stateResult = curl_exec($ch);
curl_close($ch);
$stateData = json_decode($stateResult, true);
$doorState = isset($stateData['state']) ? $stateData['state'] : 'unknown';
$stateHu = [
'open' => 'NYITVA',
'opening' => 'NYΓLIK',
'closed' => 'ZΓRVA',
'closing' => 'ZΓRΓDIK',
'unknown' => 'ISMERETLEN',
];
$doorStateLabel = isset($stateHu[$doorState]) ? $stateHu[$doorState] : strtoupper($doorState);
$message = 'Parancs elkΓΌldve. Kapu Γ‘llapota: ' . htmlspecialchars($doorStateLabel);
$messageClass = ($doorState === 'closed' || $doorState === 'closing') ? 'success' : 'warning';
$showCloseConfirm = true;
// ββ OPEN ACTION ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
} else {
$submittedPassword = isset($_POST['password']) ? trim($_POST['password']) : '';
if ($submittedPassword !== PAGE_PASSWORD) {
$message = 'Helytelen jelszΓ³.';
$messageClass = 'error';
} else {
$webhookUrl = HA_URL . '/api/webhook/' . HA_WEBHOOK_ID;
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{}');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$webhookResult = curl_exec($ch);
$webhookStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($webhookStatus >= 200 && $webhookStatus < 300) {
sleep(STATE_DELAY_SEC);
$stateUrl = HA_URL . '/api/states/' . HA_DOOR_ENTITY;
$ch = curl_init($stateUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . HA_TOKEN,
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$stateResult = curl_exec($ch);
curl_close($ch);
$stateData = json_decode($stateResult, true);
$doorState = isset($stateData['state']) ? $stateData['state'] : 'unknown';
$stateHu = [
'open' => 'NYITVA',
'opening' => 'NYΓLIK',
'closed' => 'ZΓRVA',
'closing' => 'ZΓRΓDIK',
'unknown' => 'ISMERETLEN',
];
$doorStateLabel = isset($stateHu[$doorState]) ? $stateHu[$doorState] : strtoupper($doorState);
$message = 'Parancs elkΓΌldve. Kapu Γ‘llapota: ' . htmlspecialchars($doorStateLabel);
$messageClass = ($doorState === 'open' || $doorState === 'opening') ? 'success' : 'warning';
// Show close button only if door is confirmed open/opening
if ($doorState === 'open' || $doorState === 'opening') {
$showCloseButton = true;
}
} else {
$message = 'Nem sikerΓΌlt elΓ©rni a Home Assistantt (HTTP ' . $webhookStatus . ').';
$messageClass = 'error';
}
}
}
}
?>
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GarΓ‘zskapu</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: sans-serif;
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.card {
background: #16213e;
border: 1px solid #0f3460;
border-radius: 12px;
padding: 40px 32px;
width: 100%;
max-width: 380px;
text-align: center;
}
.icon { font-size: 48px; margin-bottom: 16px; }
h1 {
color: #e0e0e0;
font-size: 1.4rem;
margin-bottom: 28px;
}
label {
display: block;
text-align: left;
color: #a0a0b0;
font-size: 0.85rem;
margin-bottom: 6px;
}
input[type="password"] {
width: 100%;
padding: 12px 14px;
border: 1px solid #0f3460;
border-radius: 8px;
background: #0f3460;
color: #e0e0e0;
font-size: 1rem;
margin-bottom: 20px;
outline: none;
}
input[type="password"]:focus {
border-color: #e94560;
}
button {
width: 100%;
padding: 13px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
letter-spacing: 0.5px;
}
.btn-open {
background: #e94560;
color: #fff;
}
.btn-open:hover { background: #c73652; }
.btn-close {
background: #ff9800;
color: #fff;
margin-top: 12px;
}
.btn-close:hover { background: #e68900; }
.message {
margin-top: 20px;
padding: 12px 16px;
border-radius: 8px;
font-size: 0.95rem;
font-weight: bold;
}
.success { background: #1a3a2a; color: #4caf50; border: 1px solid #4caf50; }
.warning { background: #3a2a1a; color: #ff9800; border: 1px solid #ff9800; }
.error { background: #3a1a1a; color: #f44336; border: 1px solid #f44336; }
</style>
</head>
<body>
<div class="card">
<div class="icon">π</div>
<h1>GarΓ‘zskapu</h1>
<?php if ($showCloseButton): ?>
<!-- Door confirmed open: show only the close button -->
<div class="message <?php echo htmlspecialchars($messageClass); ?>">
<?php echo $message; ?>
</div>
<form method="POST" action="">
<input type="hidden" name="action" value="close">
<button type="submit" class="btn-close" style="margin-top: 20px;">
GarΓ‘zskapu zΓ‘rΓ‘sa
</button>
</form>
<?php elseif ($showCloseConfirm): ?>
<!-- Close command sent: show result, no further actions -->
<div class="message <?php echo htmlspecialchars($messageClass); ?>">
<?php echo $message; ?>
</div>
<?php else: ?>
<!-- Default: show open form -->
<form method="POST" action="">
<input type="hidden" name="action" value="open">
<label for="password">JelszΓ³</label>
<input
type="password"
id="password"
name="password"
placeholder="Add meg a jelszΓ³t"
autocomplete="current-password"
required
>
<button type="submit" class="btn-open">GarΓ‘zskapu nyitΓ‘sa</button>
</form>
<?php if ($message !== ''): ?>
<div class="message <?php echo htmlspecialchars($messageClass); ?>">
<?php echo $message; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</body>
</html>
and here's the automation that goes with it.
alias: Garage Door Webhook Trigger
description: Opens garage door when webhook is called
triggers:
- webhook_id: very-long-random-char-string
allowed_methods:
- POST
local_only: false
trigger: webhook
conditions: []
actions:
- target:
entity_id: cover.smart_garage_door_opener
action: cover.open_cover
mode: single
Just make sure the devices and webhook_id match in both the php and the automation. Also, in most cases, the 'HA_URL' will need the port number - mine is behind a reverse proxy.