MQTT publish to HA
This commit is contained in:
parent
30985747a6
commit
16eace739a
2 changed files with 128 additions and 0 deletions
80
api.php
80
api.php
|
|
@ -40,6 +40,11 @@ try {
|
||||||
respond(['ok' => true, 'settings' => get_settings($db)]);
|
respond(['ok' => true, 'settings' => get_settings($db)]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'publishHa':
|
||||||
|
publish_ha_state($payload['state'] ?? []);
|
||||||
|
respond(['ok' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'saveCorrection':
|
case 'saveCorrection':
|
||||||
save_correction($db, (int)($payload['year'] ?? 0), (int)($payload['month'] ?? 0), $payload['value'] ?? null);
|
save_correction($db, (int)($payload['year'] ?? 0), (int)($payload['month'] ?? 0), $payload['value'] ?? null);
|
||||||
respond(['ok' => true, 'corrections' => get_corrections($db)]);
|
respond(['ok' => true, 'corrections' => get_corrections($db)]);
|
||||||
|
|
@ -89,6 +94,81 @@ try {
|
||||||
fail($e->getMessage(), 500);
|
fail($e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function app_config(): array
|
||||||
|
{
|
||||||
|
$defaults = [
|
||||||
|
'name' => 'GO Vacation Planner',
|
||||||
|
'topic_prefix' => 'go',
|
||||||
|
'mqtt_host' => '192.168.0.203',
|
||||||
|
'mqtt_port' => 1883,
|
||||||
|
];
|
||||||
|
$file = __DIR__ . '/config.php';
|
||||||
|
if (is_file($file)) {
|
||||||
|
$config = require $file;
|
||||||
|
if (is_array($config)) return array_replace($defaults, $config);
|
||||||
|
}
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mqtt_pub(array $config, string $topic, string $payload, bool $retain = true): void
|
||||||
|
{
|
||||||
|
$host = $config['mqtt_host'];
|
||||||
|
$port = (int)($config['mqtt_port'] ?? 1883);
|
||||||
|
$cmd = 'mosquitto_pub -h ' . escapeshellarg($host)
|
||||||
|
. ' -p ' . escapeshellarg((string)$port)
|
||||||
|
. ' -t ' . escapeshellarg($topic)
|
||||||
|
. ' -m ' . escapeshellarg($payload)
|
||||||
|
. ($retain ? ' -r' : '');
|
||||||
|
@shell_exec($cmd . ' 2>/dev/null');
|
||||||
|
}
|
||||||
|
|
||||||
|
function publish_ha_discovery(array $config): void
|
||||||
|
{
|
||||||
|
$p = trim($config['topic_prefix'], '/');
|
||||||
|
$dev = ['identifiers' => ['go_vacation_planner'], 'name' => $config['name']];
|
||||||
|
$sensors = [
|
||||||
|
'feriefridage' => ['name' => 'Feriefridage', 'unit' => 'd', 'topic' => "$p/feriefridage"],
|
||||||
|
'vacation_days' => ['name' => 'Vacation days', 'unit' => 'd', 'topic' => "$p/vacation_days"],
|
||||||
|
'flex_hours' => ['name' => 'FLEX hours', 'unit' => 'h', 'topic' => "$p/flex_hours"],
|
||||||
|
'total_days' => ['name' => 'Total available days', 'unit' => 'd', 'topic' => "$p/total_days"],
|
||||||
|
'upcoming_vacations' => ['name' => 'Upcoming vacations', 'unit' => '', 'topic' => "$p/upcoming_vacations", 'attributes_topic' => "$p/upcoming_vacations/attributes"],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($sensors as $id => $sensor) {
|
||||||
|
$payload = [
|
||||||
|
'name' => $sensor['name'],
|
||||||
|
'state_topic' => $sensor['topic'],
|
||||||
|
'unique_id' => "go_$id",
|
||||||
|
'device' => $dev,
|
||||||
|
];
|
||||||
|
if ($sensor['unit'] !== '') $payload['unit_of_measurement'] = $sensor['unit'];
|
||||||
|
if (isset($sensor['attributes_topic'])) $payload['json_attributes_topic'] = $sensor['attributes_topic'];
|
||||||
|
mqtt_pub($config, "homeassistant/sensor/go_$id/config", json_encode($payload, JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function publish_ha_state(array $state): void
|
||||||
|
{
|
||||||
|
$config = app_config();
|
||||||
|
$p = trim($config['topic_prefix'], '/');
|
||||||
|
publish_ha_discovery($config);
|
||||||
|
|
||||||
|
$numeric = ['feriefridage', 'vacation_days', 'flex_hours', 'total_days'];
|
||||||
|
foreach ($numeric as $key) {
|
||||||
|
$value = isset($state[$key]) && is_numeric($state[$key]) ? round((float)$state[$key], 2) : 0;
|
||||||
|
mqtt_pub($config, "$p/$key", (string)$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$upcoming = is_array($state['upcoming_vacations'] ?? null) ? $state['upcoming_vacations'] : [];
|
||||||
|
$first = $upcoming[0] ?? null;
|
||||||
|
$summary = is_array($first)
|
||||||
|
? (($first['title'] ?? 'Vacation') . ' ' . ($first['start'] ?? '') . ' → ' . ($first['end'] ?? ''))
|
||||||
|
: 'none';
|
||||||
|
mqtt_pub($config, "$p/upcoming_vacations", $summary);
|
||||||
|
mqtt_pub($config, "$p/upcoming_vacations/attributes", json_encode(['vacations' => $upcoming], JSON_UNESCAPED_SLASHES));
|
||||||
|
mqtt_pub($config, "$p/state", json_encode($state, JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
function db(): PDO
|
function db(): PDO
|
||||||
{
|
{
|
||||||
if (!is_dir(DB_DIR) && !mkdir(DB_DIR, 0775, true) && !is_dir(DB_DIR)) {
|
if (!is_dir(DB_DIR) && !mkdir(DB_DIR, 0775, true) && !is_dir(DB_DIR)) {
|
||||||
|
|
|
||||||
48
app.js
48
app.js
|
|
@ -93,6 +93,54 @@ function renderAll() {
|
||||||
renderWarnings();
|
renderWarnings();
|
||||||
renderEntries();
|
renderEntries();
|
||||||
renderPlanner();
|
renderPlanner();
|
||||||
|
scheduleHaPublish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleHaPublish() {
|
||||||
|
clearTimeout(scheduleHaPublish.timer);
|
||||||
|
scheduleHaPublish.timer = setTimeout(() => {
|
||||||
|
publishHaState().catch((error) => console.warn('HA publish failed', error));
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function publishHaState() {
|
||||||
|
if (!state.plan?.current) return;
|
||||||
|
const current = state.plan.current;
|
||||||
|
await post({
|
||||||
|
action: 'publishHa',
|
||||||
|
state: {
|
||||||
|
feriefridage: Number(current.feriefridage || 0),
|
||||||
|
vacation_days: Number(current.vacation || 0),
|
||||||
|
flex_hours: Number(current.flex || 0),
|
||||||
|
total_days: Number(current.totalDays || 0),
|
||||||
|
upcoming_vacations: upcomingVacationsForHa(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function upcomingVacationsForHa() {
|
||||||
|
const today = toIsoDate(new Date());
|
||||||
|
return state.entries
|
||||||
|
.filter((entry) => entry.end_date >= today)
|
||||||
|
.sort((a, b) => a.start_date.localeCompare(b.start_date) || a.id - b.id)
|
||||||
|
.map((entry) => {
|
||||||
|
const summary = state.plan.entrySummaries[entry.id] || { workdays: 0, usedVacation: 0, usedFeriefridage: 0, usedFlex: 0, usedFlexDays: 0 };
|
||||||
|
return {
|
||||||
|
title: entry.title,
|
||||||
|
start: entry.start_date,
|
||||||
|
end: entry.end_date,
|
||||||
|
start_display: formatDate(entry.start_date),
|
||||||
|
end_display: formatDate(entry.end_date),
|
||||||
|
status: entry.status,
|
||||||
|
source: entry.source,
|
||||||
|
workdays: Number(summary.workdays || 0),
|
||||||
|
vacation_days: Number(summary.usedVacation || 0),
|
||||||
|
feriefridage: Number(summary.usedFeriefridage || 0),
|
||||||
|
flex_hours: Number(summary.usedFlex || 0),
|
||||||
|
flex_days: Number(summary.usedFlexDays || 0),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillSettingsForm() {
|
function fillSettingsForm() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue