Improve Home Assistant notifications
This commit is contained in:
+167
-44
@@ -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),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user