feat: add ha_info, ha_shopping_items endpoints and avahi mDNS service file for HACS integration
- api/index.php: new haGetInfo() endpoint (unique_id, version, instance, items count) - api/index.php: new haGetShoppingItems() endpoint (Bring! + internal shopping list) - api/index.php: haInventorySensor() now accepts ?expiry_days=N query param - api/cron_smart_shopping.php: auto-register avahi mDNS service if avahi-daemon present - docker/avahi-evershelf.xml: Zeroconf _evershelf._tcp service declaration - .env.example: add INSTANCE_NAME variable (used by HA integration for device label)
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.
|
# Leave empty to allow anyone with access to the server to change settings.
|
||||||
SETTINGS_TOKEN=
|
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 ────────────────────────────────────────────────
|
# ── Home Assistant Integration ────────────────────────────────────────────────
|
||||||
# All HA settings can also be configured from the Settings → 🏠 tab.
|
# 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";
|
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());
|
haInventorySensor(getDB());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'ha_info':
|
||||||
|
haGetInfo(getDB());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ha_shopping_items':
|
||||||
|
haGetShoppingItems(getDB());
|
||||||
|
break;
|
||||||
|
|
||||||
case 'ha_test':
|
case 'ha_test':
|
||||||
haTestConnection();
|
haTestConnection();
|
||||||
break;
|
break;
|
||||||
@@ -1350,11 +1358,12 @@ function haInventorySensor(PDO $db): void {
|
|||||||
header('Access-Control-Allow-Origin: *');
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
$sensor = strtolower(trim($_GET['sensor'] ?? 'overview'));
|
$sensor = strtolower(trim($_GET['sensor'] ?? 'overview'));
|
||||||
|
$expiryDays = max(1, min(90, (int)($_GET['expiry_days'] ?? env('HA_EXPIRY_DAYS', 3))));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$expiring = (int)$db->query(
|
$expiring = (int)$db->query(
|
||||||
"SELECT COUNT(*) FROM inventory WHERE quantity > 0 AND expiry_date IS NOT NULL
|
"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();
|
)->fetchColumn();
|
||||||
|
|
||||||
$expired = (int)$db->query(
|
$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) =====
|
// ===== FOOD FACTS (cached daily) =====
|
||||||
function getFoodFacts(): void {
|
function getFoodFacts(): void {
|
||||||
EverLog::info('getFoodFacts');
|
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