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
JavaScript
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.`);
}
}
}