chore: auto-merge develop → main
Triggered by: b2e0f6d feat: add ha_info, ha_shopping_items endpoints and avahi mDNS service file for HACS integration
This commit is contained in:
@@ -129,6 +129,11 @@ GDRIVE_RETENTION_DAYS=30
|
||||
# Leave empty to allow anyone with access to the server to change settings.
|
||||
SETTINGS_TOKEN=
|
||||
|
||||
# INSTANCE_NAME: display name for this EverShelf instance (used by the HA integration
|
||||
# for Zeroconf discovery label and device name in Home Assistant).
|
||||
# Defaults to the server hostname if left empty.
|
||||
INSTANCE_NAME=
|
||||
|
||||
# ── Home Assistant Integration ────────────────────────────────────────────────
|
||||
# All HA settings can also be configured from the Settings → 🏠 tab.
|
||||
#
|
||||
|
||||
@@ -197,3 +197,23 @@ if (env('HA_ENABLED', 'false') === 'true' && env('HA_WEBHOOK_ID', '') !== '') {
|
||||
echo '[' . date('Y-m-d H:i:s') . '] HA expiry hook warning: ' . $haE->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Avahi/mDNS discovery registration ─────────────────────────────────────────
|
||||
// If avahi-daemon is running on this host, register the _evershelf._tcp service
|
||||
// so that Home Assistant can auto-discover this instance via Zeroconf.
|
||||
if (function_exists('shell_exec')) {
|
||||
try {
|
||||
$avahiService = '/etc/avahi/services/evershelf.xml';
|
||||
// Only create/update if avahi-daemon is installed and the file doesn't exist yet
|
||||
if (!file_exists($avahiService) && (shell_exec('which avahi-daemon 2>/dev/null') || shell_exec('which avahi-publish 2>/dev/null'))) {
|
||||
$template = __DIR__ . '/../docker/avahi-evershelf.xml';
|
||||
if (file_exists($template)) {
|
||||
$xml = file_get_contents($template);
|
||||
@file_put_contents($avahiService, $xml);
|
||||
echo '[' . date('Y-m-d H:i:s') . '] Avahi mDNS service registered at ' . $avahiService . "\n";
|
||||
}
|
||||
}
|
||||
} catch (Throwable $avahiE) {
|
||||
// Non-fatal: avahi not available
|
||||
}
|
||||
}
|
||||
|
||||
+74
-1
@@ -931,6 +931,14 @@ try {
|
||||
haInventorySensor(getDB());
|
||||
break;
|
||||
|
||||
case 'ha_info':
|
||||
haGetInfo(getDB());
|
||||
break;
|
||||
|
||||
case 'ha_shopping_items':
|
||||
haGetShoppingItems(getDB());
|
||||
break;
|
||||
|
||||
case 'ha_test':
|
||||
haTestConnection();
|
||||
break;
|
||||
@@ -1350,11 +1358,12 @@ function haInventorySensor(PDO $db): void {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
|
||||
$sensor = strtolower(trim($_GET['sensor'] ?? 'overview'));
|
||||
$expiryDays = max(1, min(90, (int)($_GET['expiry_days'] ?? env('HA_EXPIRY_DAYS', 3))));
|
||||
|
||||
try {
|
||||
$expiring = (int)$db->query(
|
||||
"SELECT COUNT(*) FROM inventory WHERE quantity > 0 AND expiry_date IS NOT NULL
|
||||
AND expiry_date BETWEEN date('now') AND date('now', '+3 days')"
|
||||
AND expiry_date BETWEEN date('now') AND date('now', '+{$expiryDays} days')"
|
||||
)->fetchColumn();
|
||||
|
||||
$expired = (int)$db->query(
|
||||
@@ -1469,6 +1478,70 @@ function haTestConnection(): void {
|
||||
}
|
||||
|
||||
|
||||
// ===== HA DISCOVERY INFO =====
|
||||
|
||||
/**
|
||||
* Returns device info for HA Zeroconf discovery confirmation.
|
||||
* GET /api/index.php?action=ha_info
|
||||
* Response: { name, instance, version, unique_id, has_token, api_version, items_count }
|
||||
*/
|
||||
function haGetInfo(PDO $db): void {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
// Stable unique_id derived from server identity (survives restarts)
|
||||
$uniqueId = 'evershelf_' . substr(md5(__DIR__ . php_uname('n')), 0, 12);
|
||||
$itemsCount = (int)$db->query("SELECT COUNT(*) FROM inventory WHERE quantity > 0")->fetchColumn();
|
||||
echo json_encode([
|
||||
'name' => 'EverShelf',
|
||||
'instance' => env('INSTANCE_NAME', php_uname('n')),
|
||||
'version' => _appVersion(),
|
||||
'unique_id' => $uniqueId,
|
||||
'has_token' => !empty(env('SETTINGS_TOKEN', '')),
|
||||
'api_version' => 1,
|
||||
'items_count' => $itemsCount,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns shopping list items in a clean format suitable for HA todo entity.
|
||||
* GET /api/index.php?action=ha_shopping_items
|
||||
* Response: { items: [{id, name, note}], count, mode }
|
||||
*/
|
||||
function haGetShoppingItems(PDO $db): void {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
try {
|
||||
if (isShoppingBringMode()) {
|
||||
$auth = bringAuth();
|
||||
if (!$auth) {
|
||||
echo json_encode(['items' => [], 'count' => 0, 'mode' => 'bring']);
|
||||
return;
|
||||
}
|
||||
$listData = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$auth['bringListUUID']}");
|
||||
$items = array_map(fn($r) => [
|
||||
'id' => $r['uuid'] ?? md5(($r['name'] ?? '') . uniqid()),
|
||||
'name' => $r['name'] ?? '',
|
||||
'note' => $r['specification'] ?? '',
|
||||
], $listData['purchase'] ?? []);
|
||||
echo json_encode(['items' => $items, 'count' => count($items), 'mode' => 'bring'], JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$rows = $db->query(
|
||||
"SELECT rowid AS id, name, specification AS note FROM shopping_list ORDER BY sort_order ASC, added_at ASC"
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
$items = array_map(fn($r) => [
|
||||
'id' => (string)$r['id'],
|
||||
'name' => $r['name'],
|
||||
'note' => $r['note'] ?? '',
|
||||
], $rows);
|
||||
echo json_encode(['items' => $items, 'count' => count($items), 'mode' => 'internal'], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== FOOD FACTS (cached daily) =====
|
||||
function getFoodFacts(): void {
|
||||
EverLog::info('getFoodFacts');
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" standalone='no'?>
|
||||
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
||||
<service-group>
|
||||
<name replace-wildcards="yes">EverShelf Pantry (%h)</name>
|
||||
<service>
|
||||
<type>_evershelf._tcp</type>
|
||||
<port>80</port>
|
||||
<txt-record>path=/api/</txt-record>
|
||||
<txt-record>version=1.0</txt-record>
|
||||
<txt-record>app=evershelf</txt-record>
|
||||
</service>
|
||||
</service-group>
|
||||
Reference in New Issue
Block a user