Improve Home Assistant notifications

This commit is contained in:
seo
2026-06-21 07:46:28 +09:00
parent 49394a8ee5
commit 772010562a
6 changed files with 269 additions and 55 deletions
+167 -44
View File
@@ -22,7 +22,10 @@ define('DB_PORT', (int)($controlDbConfig['port'] ?? 3306));
define('DB_NAME', (string)($controlDbConfig['name'] ?? 'fanpanel'));
define('DB_USER', (string)($controlDbConfig['user'] ?? 'fanpanel'));
define('DB_PASS', (string)($controlDbConfig['pass'] ?? ''));
define('HA_NOTIFY_WEBHOOK_ID', (string)($controlHaNotifyConfig['webhook_id'] ?? 'control_notify_20260621'));
define('HA_NOTIFY_WEBHOOK_ID', (string)($controlHaNotifyConfig['webhook_id'] ?? ''));
define('HA_NOTIFY_BATTERY_LOW_COOLDOWN', max(1, (int)($controlHaNotifyConfig['cooldowns']['battery_low'] ?? 10)));
define('HA_NOTIFY_BATTERY_CLEAR_COOLDOWN', max(1, (int)($controlHaNotifyConfig['cooldowns']['battery_clear'] ?? 300)));
define('HA_NOTIFY_SYSTEM_NOTICE_COOLDOWN', max(1, (int)($controlHaNotifyConfig['cooldowns']['system_notice'] ?? 600)));
$batteryCellCapacityMah = max(0.0, (float)($controlBatteryConfig['cell_capacity_mah'] ?? 0.0));
$batteryParallelCells = max(1, (int)($controlBatteryConfig['parallel_cells'] ?? 1));
@@ -521,7 +524,7 @@ function ha_notify_default_data(string $level, string $tag, string $url = '/'):
'tag' => $tag,
'group' => 'control',
'sticky' => $isCritical ? 'true' : 'false',
'persistent' => $isCritical ? 'true' : 'false',
'persistent' => $isCritical,
'renotify' => 'true',
'color' => $isCritical ? '#ff3b30' : ($isWarning ? '#ffcc00' : '#3b82f6'),
'notification_icon' => $isCritical ? 'mdi:alert-circle' : ($isWarning ? 'mdi:alert' : 'mdi:information'),
@@ -541,18 +544,62 @@ function ha_notify_default_data(string $level, string $tag, string $url = '/'):
];
}
function ha_notify_log_rows(int $limit = 20): array
{
$limit = max(1, min(100, $limit));
$stmt = db()->query("
SELECT
id,
created_at,
event,
target,
title,
tag,
http_code,
error,
meta
FROM ha_notify_logs
ORDER BY id DESC
LIMIT " . $limit . "
");
$rows = [];
foreach ($stmt->fetchAll() as $row) {
$meta = json_decode((string)($row['meta'] ?? ''), true);
$data = is_array($meta['data'] ?? null) ? $meta['data'] : [];
$rows[] = [
'id' => (int)$row['id'],
'created_at' => (string)$row['created_at'],
'event' => (string)$row['event'],
'target' => $row['target'],
'title' => $row['title'],
'tag' => $row['tag'],
'http_code' => $row['http_code'] === null ? null : (int)$row['http_code'],
'error' => $row['error'],
'level' => (string)($data['level'] ?? ''),
'channel' => (string)($data['channel'] ?? ''),
'icon' => (string)($data['notification_icon'] ?? ''),
];
}
return $rows;
}
function send_ha_notify(array $payload): array
{
$title = mb_substr((string)($payload['title'] ?? APP_NAME), 0, 255);
$message = mb_substr((string)($payload['message'] ?? $payload['body'] ?? ''), 0, 4000);
$level = (string)($payload['level'] ?? 'info');
$tag = mb_substr((string)($payload['tag'] ?? 'control-notify'), 0, 128);
$logTag = mb_substr((string)($payload['log_tag'] ?? $tag), 0, 128);
$url = (string)($payload['url'] ?? '/');
$data = ha_notify_default_data($level, $tag, $url);
if (is_array($payload['data'] ?? null)) {
$data = array_replace_recursive($data, $payload['data']);
}
$data['level'] = $level;
$body = json_encode([
'title' => $title,
@@ -560,6 +607,25 @@ function send_ha_notify(array $payload): array
'data' => $data,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (HA_NOTIFY_WEBHOOK_ID === '') {
ha_notify_log('send_failed', [
'target' => null,
'title' => $title,
'tag' => $logTag,
'http_code' => null,
'error' => 'ha_notify_webhook_missing',
'data' => $data,
]);
return [
'sent' => 0,
'failed' => 0,
'target' => null,
'http_code' => null,
'error' => 'ha_notify_webhook_missing',
];
}
$attempts = 0;
foreach (ha_notify_targets() as $target) {
$name = (string)($target['name'] ?? 'ha');
@@ -587,7 +653,7 @@ function send_ha_notify(array $payload): array
ha_notify_log('send_success', [
'target' => $name,
'title' => $title,
'tag' => $tag,
'tag' => $logTag,
'http_code' => $httpCode,
'data' => $data,
]);
@@ -603,7 +669,7 @@ function send_ha_notify(array $payload): array
ha_notify_log('send_failed', [
'target' => $name,
'title' => $title,
'tag' => $tag,
'tag' => $logTag,
'http_code' => $httpCode,
'error' => $error ?: ('HTTP ' . $httpCode),
]);
@@ -618,41 +684,85 @@ function send_ha_notify(array $payload): array
];
}
function battery_soc_rising_from_history(float $percent): bool
function clear_ha_notification(string $tag, string $title = 'Control 알림 해제'): array
{
$stmt = db()->query("
SELECT battery_percent
FROM sensor_logs
WHERE battery_percent IS NOT NULL
ORDER BY id DESC
LIMIT 20
");
$rows = array_reverse($stmt->fetchAll());
return send_ha_notify([
'title' => $title,
'message' => 'clear_notification',
'level' => 'info',
'tag' => $tag,
'log_tag' => $tag . '-clear',
'data' => [
'tag' => $tag,
'channel' => 'control_status',
'notification_icon' => 'mdi:bell-check',
'persistent' => false,
'sticky' => 'false',
'renotify' => 'false',
'timeout' => 1,
],
]);
}
if (count($rows) < 8) {
return false;
function battery_low_notify_profile(float $percent): array
{
if ($percent <= 5.0) {
return [
'label' => '긴급',
'level' => 'critical',
'channel' => 'control_critical',
'importance' => 'max',
'icon' => 'mdi:battery-alert-variant-outline',
'color' => '#b91c1c',
'sticky' => 'true',
'persistent' => true,
'vibration' => '900,200,900,200,1400,250,1800',
'led' => 'red',
];
}
$values = [];
foreach ($rows as $row) {
$value = $row['battery_percent'] ?? null;
if ($value === null || $value === '' || !is_numeric($value)) {
continue;
}
$values[] = (float)$value;
if ($percent <= 10.0) {
return [
'label' => '위험',
'level' => 'critical',
'channel' => 'control_battery_critical',
'importance' => 'max',
'icon' => 'mdi:battery-10',
'color' => '#dc2626',
'sticky' => 'true',
'persistent' => true,
'vibration' => '900,250,900,250,1400',
'led' => 'red',
];
}
if (count($values) < 8) {
return false;
if ($percent <= 15.0) {
return [
'label' => '경고',
'level' => 'warning',
'channel' => 'control_battery_warning',
'importance' => 'high',
'icon' => 'mdi:battery-20',
'color' => '#f97316',
'sticky' => 'true',
'persistent' => true,
'vibration' => '600,200,900,200,900',
'led' => 'yellow',
];
}
$older = array_slice($values, 0, 5);
$recent = array_slice($values, -5);
$olderAvg = array_sum($older) / count($older);
$recentAvg = array_sum($recent) / count($recent);
return ($recentAvg - $olderAvg) >= 0.20
&& ($percent - $olderAvg) >= 0.15;
return [
'label' => '주의',
'level' => 'warning',
'channel' => 'control_battery',
'importance' => 'high',
'icon' => 'mdi:battery-alert',
'color' => '#f59e0b',
'sticky' => 'false',
'persistent' => false,
'vibration' => '250,150,250',
'led' => 'yellow',
];
}
function send_battery_low_notify_if_needed(array $battery): array
@@ -671,7 +781,18 @@ function send_battery_low_notify_if_needed(array $battery): array
? (float)$battery['voltage']
: null;
$tag = 'control-battery-low';
if ($percent > 20.0) {
$latestLow = latest_ha_notify_epoch_by_tag($tag);
$latestClear = latest_ha_notify_epoch_by_tag($tag . '-clear');
if (
$latestLow > 0
&& $latestLow > $latestClear
&& ha_notify_due($tag . '-clear', HA_NOTIFY_BATTERY_CLEAR_COOLDOWN)
) {
return clear_ha_notification($tag, '배터리 SOC 복구');
}
return [
'sent' => 0,
'failed' => 0,
@@ -679,8 +800,7 @@ function send_battery_low_notify_if_needed(array $battery): array
];
}
$tag = 'control-battery-low';
if (!ha_notify_due($tag, 10)) {
if (!ha_notify_due($tag, HA_NOTIFY_BATTERY_LOW_COOLDOWN)) {
return [
'sent' => 0,
'failed' => 0,
@@ -688,28 +808,31 @@ function send_battery_low_notify_if_needed(array $battery): array
];
}
$profile = battery_low_notify_profile($percent);
$body = 'Battery SOC '
. number_format($percent, 2)
. '%'
. ($voltage === null ? '' : ' / ' . number_format($voltage, 3) . 'V')
. "\n20% 이하 상태입니다.";
. "\n"
. $profile['label']
. ' 단계입니다.';
return send_ha_notify([
'title' => '배터리 SOC 경고',
'title' => '배터리 SOC ' . $profile['label'],
'message' => $body,
'level' => 'critical',
'level' => $profile['level'],
'url' => '/',
'tag' => $tag,
'data' => [
'channel' => 'control_battery',
'notification_icon' => 'mdi:battery-alert',
'color' => '#ff3b30',
'sticky' => 'true',
'persistent' => 'true',
'channel' => $profile['channel'],
'notification_icon' => $profile['icon'],
'color' => $profile['color'],
'sticky' => $profile['sticky'],
'persistent' => $profile['persistent'],
'renotify' => 'true',
'importance' => 'high',
'vibrationPattern' => '900,250,900,250,1400',
'ledColor' => 'red',
'importance' => $profile['importance'],
'vibrationPattern' => $profile['vibration'],
'ledColor' => $profile['led'],
'battery_percent' => round($percent, 2),
'battery_voltage' => $voltage === null ? null : round($voltage, 3),
],