config() === null) { return self::SUCCESS; } $this->sweepOffline($notifier); $this->sweepBatteries($notifier); return self::SUCCESS; } private function sweepOffline(NotificationService $notifier): void { foreach (Device::where('status', 'active')->where('demo', false)->get() as $device) { $key = "notified:offline:{$device->id}"; if (! $device->isOnline()) { if (! Cache::has($key) && $notifier->notify('device_offline', __('notifications.device_offline_subject', ['name' => $device->name]), __('notifications.device_offline_body', ['name' => $device->name]))) { // Flag stays until the condition RESOLVES (cleared below) — one mail per // continuous outage, not one every 24h. The long TTL is just a leak guard. Cache::put($key, true, now()->addMonth()); } } else { Cache::forget($key); } } } private function sweepBatteries(NotificationService $notifier): void { $batteries = Entity::where('type', 'battery')->with('device', 'state')->get(); foreach ($batteries as $entity) { if ($entity->device === null || $entity->device->demo) { continue; } // Missing reading defaults to 100 (no alert); a genuine 0% is a dead battery and MUST // alert — so the threshold is simply < 20 (a real 0 included). $pct = (int) data_get($entity->state, 'state.percent', 100); $key = "notified:battery:{$entity->id}"; if ($pct < 20) { if (! Cache::has($key) && $notifier->notify('low_battery', __('notifications.low_battery_subject', ['name' => $entity->device->name]), __('notifications.low_battery_body', ['name' => $entity->device->name, 'percent' => $pct]))) { Cache::put($key, true, now()->addMonth()); } } elseif ($pct >= 25) { Cache::forget($key); // hysteresis so it doesn't flap around the threshold } } } }