UNPKG

homebridge-azan

Version:

Homebridge plugin to trigger HomeKit events for Muslim prayer (Azan) times, enabling HomePod audio automations.

229 lines (193 loc) 9.18 kB
const axios = require('axios'); const moment = require('moment-timezone'); let Service, Characteristic; module.exports = (api) => { Service = api.hap.Service; Characteristic = api.hap.Characteristic; // Register the main platform for the plugin. api.registerPlatform("Azan", PrayerTimesPlatform); }; class PrayerTimesPlatform { constructor(log, config, api) { this.log = log; this.config = config; this.api = api; this.prayerTimes = {}; // Stores prayer times for the current day this.lastFetchedDate = null; // Date for which prayer times were last fetched this.triggeredPrayersToday = new Set(); // Stores triggered prayers for the current day this.locationTimezone = null; // Timezone fetched from AlAdhan API this.nextPrayerTimeAccessory = null; // To hold the dedicated 'Next Prayer Time' accessory // Configuration defaults this.platformName = config.name || "Muslim Prayer Times"; this.city = config.city; this.country = config.country; this.calculationMethod = config.calculationMethod || 2; // Default to ISNA this.pollingIntervalMinutes = config.pollingIntervalMinutes || 1; // Check every minute this.debug = config.debug || false; // Validate configuration if (!this.city || !this.country) { this.log.error("Configuration Error: 'city' and 'country' are required."); return; } this.accessories = new Map(); // Map to store HomeKit accessories by UUID // This event is fired when Homebridge is ready to publish accessories. this.api.on('didFinishLaunching', () => { this.log.debug("didFinishLaunching event fired."); this.discoverPrayerTimeSwitches(); this.schedulePrayerChecks(); }); } /** * This function is invoked when Homebridge restores cached accessories from disk. * It should be used to setup event handlers for characteristics and update values. */ configureAccessory(accessory) { this.log.info(`Loading accessory from cache: ${accessory.displayName}`); this.accessories.set(accessory.UUID, accessory); } /** * Creates or restores HomeKit accessories for each prayer time (Stateless Programmable Switches). */ discoverPrayerTimeSwitches() { const prayerNames = ["Fajr", "Dhuhr", "Asr", "Maghrib", "Isha"]; for (const prayerName of prayerNames) { const uuid = this.api.hap.uuid.generate(prayerName + this.platformName); let accessory = this.accessories.get(uuid); if (!accessory) { this.log.info(`Adding new accessory: ${prayerName}`); accessory = new this.api.platformAccessory(prayerName, uuid); accessory.context.prayerName = prayerName; // Register the accessory with the plugin and platform name. this.api.registerPlatformAccessories("homebridge-azan", "Azan", [accessory]); } else { this.log.info(`Restoring existing accessory: ${prayerName}`); } // Ensure the Stateless Programmable Switch service exists. // This service is used to trigger HomeKit automations. let service = accessory.getService(Service.StatelessProgrammableSwitch); if (!service) { service = accessory.addService(Service.StatelessProgrammableSwitch, prayerName, prayerName); } // Set the name visible in HomeKit for this switch. service.setCharacteristic(Characteristic.Name, prayerName); // Store the accessory for later use. this.accessories.set(uuid, accessory); } } /** * Fetches prayer times from the AlAdhan API. */ async fetchPrayerTimes(city, country, method) { const todayDateStr = moment().format("DD-MM-YYYY"); const fullApiUrl = `https://api.aladhan.com/v1/timingsByCity/${todayDateStr}`; const params = { city: city, country: country, method: method, }; this.log.debug(`Fetching prayer times for ${todayDateStr} (City: ${city}, Country: ${country})...`); try { const response = await axios.get(fullApiUrl, { params }); const data = response.data; if (data && data.data) { const timings = data.data.timings; const meta = data.data.meta; this.locationTimezone = moment.tz.zone(meta.timezone); if (!this.locationTimezone) { this.log.error(`Failed to load timezone data for: ${meta.timezone}`); return null; } this.log.info(`Prayer times fetched successfully for timezone: ${meta.timezone}`); return { Fajr: timings.Fajr, Dhuhr: timings.Dhuhr, Asr: timings.Asr, Maghrib: timings.Maghrib, Isha: timings.Isha, }; } else { this.log.error("Error: No data found in AlAdhan API response or 'data' key is missing."); return null; } } catch (error) { this.log.error(`Error fetching prayer times: ${error.message}`); if (error.response) { this.log.error(`API Response Status: ${error.response.status}`); this.log.error(`API Response Data: ${JSON.stringify(error.response.data)}`); } return null; } } /** * Schedules the continuous checking of prayer times and updates the next prayer time accessory. */ schedulePrayerChecks() { this.log.info(`Starting prayer time monitoring. Checking every ${this.pollingIntervalMinutes} minute(s).`); setInterval(async () => { const currentMoment = moment(); // Get current moment in local timezone initially // Check if it's a new day or if prayer times haven't been fetched yet if (!this.lastFetchedDate || currentMoment.date() !== this.lastFetchedDate.date()) { this.log.info(`\n--- New day detected: ${currentMoment.format("YYYY-MM-DD")} ---`); this.prayerTimes = await this.fetchPrayerTimes(this.city, this.country, this.calculationMethod); if (this.prayerTimes) { this.lastFetchedDate = currentMoment; // Set last fetched date to current moment this.triggeredPrayersToday.clear(); // Reset triggered prayers for the new day this.log.info("Today's prayer times:"); for (const prayer in this.prayerTimes) { this.log.info(` ${prayer}: ${this.prayerTimes[prayer]}`); } } else { this.log.error("Could not fetch prayer times for the new day. Will retry."); return; // Skip this check cycle } } // Ensure we have prayer times and a valid timezone before proceeding if (!this.prayerTimes || !this.locationTimezone) { this.log.debug("Prayer times or timezone not available yet. Waiting for next check..."); return; } // Get current time in the determined location's timezone const nowInLocationTimezone = moment().tz(this.locationTimezone.name); this.log.debug(`Current time in location: ${nowInLocationTimezone.format("HH:mm:ss")}`); // Check each prayer time for triggering automations for (const prayerName in this.prayerTimes) { const prayerTimeStr = this.prayerTimes[prayerName]; if (prayerTimeStr && !this.triggeredPrayersToday.has(prayerName)) { try { // Create a moment object for the prayer time in the location's timezone const prayerMoment = moment.tz(`${nowInLocationTimezone.format("YYYY-MM-DD")} ${prayerTimeStr}`, "YYYY-MM-DD HH:mm", this.locationTimezone.name); this.log.debug(`Checking ${prayerName}: Prayer time ${prayerMoment.format("HH:mm:ss")}, Current time ${nowInLocationTimezone.format("HH:mm:ss")}`); // Check if current time is within the trigger window (e.g., the exact minute of the prayer) if (nowInLocationTimezone.isSameOrAfter(prayerMoment, 'minute') && nowInLocationTimezone.isBefore(prayerMoment.clone().add(1, 'minute'))) { this.log.info(`It's time for ${prayerName} (${prayerTimeStr})! Triggering HomeKit event.`); this.triggerHomeKitEvent(prayerName); this.triggeredPrayersToday.add(prayerName); } } catch (e) { this.log.error(`Error processing ${prayerName} time '${prayerTimeStr}': ${e.message}`); } } } }, this.pollingIntervalMinutes * 60 * 1000); } /** * Triggers the HomeKit event for a specific prayer (Stateless Programmable Switch). */ triggerHomeKitEvent(prayerName) { const uuid = this.api.hap.uuid.generate(prayerName + this.platformName); const accessory = this.accessories.get(uuid); if (accessory) { const service = accessory.getService(Service.StatelessProgrammableSwitch); if (service) { // Trigger a single press event (value 0) // This will activate any HomeKit automation configured for this switch. service.updateCharacteristic(Characteristic.ProgrammableSwitchEvent, Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); this.log.info(`HomeKit event triggered for ${prayerName}.`); } else { this.log.error(`Service for ${prayerName} not found on accessory.`); } } else { this.log.error(`Accessory for ${prayerName} not found.`); } } }