diff --git a/README.md b/README.md index 9987da2..75c22a7 100644 --- a/README.md +++ b/README.md @@ -224,4 +224,17 @@ EOF chown -R pi:pi /home/pi/.config ``` +--- + +## 🏠 Homebridge Plugin & HomePod + +You can also use the included Homebridge plugin to integrate with Apple HomeKit. This allows you to trigger automations (like playing audio on your HomePod) when Adhan time starts. + +See [homebridge-plugin/README.md](homebridge-plugin/README.md) for full instructions. + +### Quick Start +1. Install the plugin (located in `homebridge-plugin`). +2. Configure it in Homebridge. +3. Create an automation in the Apple Home app: "When 'Adhan Trigger' Turns On, Play Audio on HomePod". + Set–and–forget – your Pi will ring adhans, scrape upcoming Jumʿah, and display both on screen every day! diff --git a/homebridge-plugin/README.md b/homebridge-plugin/README.md new file mode 100644 index 0000000..72ad2fc --- /dev/null +++ b/homebridge-plugin/README.md @@ -0,0 +1,76 @@ +# Homebridge Raspberry Pi Adhan + +This Homebridge plugin turns your Raspberry Pi into a smart Adhan clock. It calculates prayer times and exposes a switch in HomeKit that turns on when it's time for Adhan. You can use this switch to trigger automations, such as playing the Adhan on your HomePod. + +It also supports playing the Adhan audio locally on the Raspberry Pi (just like the original Python script). + +## Installation + +Since this plugin is part of the repository, you can install it by linking it to Homebridge. + +1. Navigate to the `homebridge-plugin` directory: + ```bash + cd /path/to/rasppi-adhan/homebridge-plugin + ``` +2. Install dependencies: + ```bash + npm install + ``` +3. Link the plugin to Homebridge: + ```bash + sudo npm link + ``` + *Note: Depending on your Homebridge setup, you might not need `sudo`. If you are using the official Homebridge Raspberry Pi Image, you might need to install it in the global modules directory.* + +4. Add the platform to your Homebridge `config.json` (or use the UI): + ```json + { + "platforms": [ + { + "platform": "RasppiAdhan", + "latitude": 30.1234, + "longitude": -90.1234, + "method": "NorthAmerica", + "playAudio": true, + "audioDevice": "alsa/plughw:1,0", + "mediaPath": "/home/pi/rasppi-adhan/media" + } + ] + } + ``` + +## Configuration Options + +* `latitude`: Your location latitude. +* `longitude`: Your location longitude. +* `method`: Calculation method (e.g., `NorthAmerica`, `MuslimWorldLeague`, `Egyptian`, etc.). +* `playAudio`: Set to `true` to play audio via the Pi's audio output (requires `mpv` installed). +* `audioDevice`: The audio device string for `mpv` (default: `alsa/plughw:1,0`). +* `mediaPath`: Path to the directory containing Adhan MP3 files. + +## HomePod Integration (HomeKit Automation) + +To play Adhan on your HomePod when the time comes: + +1. Open the **Apple Home** app. +2. Go to the **Automation** tab. +3. Tap **+** to add a new automation. +4. Select **A Sensor Detects Something** (or **An Accessory is Controlled** if you see the switch). + * *Note: The plugin exposes a "Switch". If you don't see it as a sensor, choose "An Accessory is Controlled".* +5. Select the **Adhan Trigger** switch. +6. Choose **Turns On**. +7. Tap **Next**. +8. Select your **HomePod** (or multiple HomePods) as the accessory to control. +9. Tap **Next**. +10. Under **Media**, choose **Play Audio**. +11. You can select **Choose Audio** to pick a specific track from Apple Music, or rely on a Shortcut. + * *Tip: For custom audio files, you might need to use "Convert to Shortcut" in the automation action, then use the "Get Contents of URL" and "Play Sound" actions, or add the Adhan audio to your Apple Music library.* + +## Local Audio Playback + +If `playAudio` is enabled, the plugin will attempt to play MP3 files from the `mediaPath`. It looks for files starting with `Adhan` and selects one randomly. It distinguishes between `Fajr` (filenames containing "fajr") and other prayers. + +Ensure `mpv` is installed on your Raspberry Pi: +```bash +sudo apt install mpv +``` diff --git a/homebridge-plugin/config.schema.json b/homebridge-plugin/config.schema.json new file mode 100644 index 0000000..ee0cb1e --- /dev/null +++ b/homebridge-plugin/config.schema.json @@ -0,0 +1,58 @@ +{ + "pluginAlias": "RasppiAdhan", + "pluginType": "platform", + "schema": { + "type": "object", + "properties": { + "latitude": { + "title": "Latitude", + "type": "number", + "required": true, + "description": "Latitude of your location (e.g. 30.345621)" + }, + "longitude": { + "title": "Longitude", + "type": "number", + "required": true, + "description": "Longitude of your location (e.g. -97.512126)" + }, + "method": { + "title": "Calculation Method", + "type": "string", + "default": "NorthAmerica", + "enum": [ + "MuslimWorldLeague", + "Egyptian", + "Karachi", + "UmmAlQura", + "Dubai", + "MoonsightingCommittee", + "NorthAmerica", + "Kuwait", + "Qatar", + "Singapore", + "Tehran", + "Turkey" + ], + "description": "Method for calculating prayer times." + }, + "playAudio": { + "title": "Play Audio Locally", + "type": "boolean", + "default": true, + "description": "If true, plays the Adhan audio on the device running Homebridge (Raspberry Pi)." + }, + "audioDevice": { + "title": "Audio Device", + "type": "string", + "default": "alsa/plughw:1,0", + "description": "Audio device for mpv (e.g. alsa/plughw:1,0). Leave default if unsure." + }, + "mediaPath": { + "title": "Media Path", + "type": "string", + "description": "Absolute path to the media directory containing mp3 files. Defaults to ../media relative to the plugin." + } + } + } +} diff --git a/homebridge-plugin/index.js b/homebridge-plugin/index.js new file mode 100644 index 0000000..1b9c70c --- /dev/null +++ b/homebridge-plugin/index.js @@ -0,0 +1,182 @@ +const adhan = require('adhan'); +const schedule = require('node-schedule'); +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +let Service, Characteristic; + +module.exports = (api) => { + Service = api.hap.Service; + Characteristic = api.hap.Characteristic; + api.registerPlatform('homebridge-rasppi-adhan', 'RasppiAdhan', RasppiAdhanPlatform); +}; + +class RasppiAdhanPlatform { + constructor(log, config, api) { + this.log = log; + this.config = config || {}; + this.api = api; + this.accessories = []; + + this.lat = this.config.latitude; + this.lon = this.config.longitude; + this.method = this.config.method || 'ISNA'; + this.playAudio = this.config.playAudio !== false; // Default true + this.audioDevice = this.config.audioDevice || 'alsa/plughw:1,0'; + this.mediaPath = this.config.mediaPath || path.join(__dirname, '../media'); + + // Map string method to Adhan constants + this.calculationMethod = adhan.CalculationMethod[this.method] || adhan.CalculationMethod.NorthAmerica; + + if (!this.lat || !this.lon) { + this.log.error('Latitude and Longitude are required!'); + return; + } + + this.api.on('didFinishLaunching', () => { + this.discoverDevices(); + this.schedulePrayers(); + // Reschedule every day at 1 AM + schedule.scheduleJob('0 1 * * *', () => { + this.schedulePrayers(); + }); + }); + } + + configureAccessory(accessory) { + this.accessories.push(accessory); + } + + discoverDevices() { + const uuid = this.api.hap.uuid.generate('rasppi-adhan-device'); + const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); + + if (existingAccessory) { + this.setupAccessory(existingAccessory); + } else { + const accessory = new this.api.platformAccessory('Adhan Clock', uuid); + this.setupAccessory(accessory); + this.api.registerPlatformAccessories('homebridge-rasppi-adhan', 'RasppiAdhan', [accessory]); + } + } + + setupAccessory(accessory) { + this.accessory = accessory; + + // Switch Service + this.switchService = accessory.getService(Service.Switch) || accessory.addService(Service.Switch, 'Adhan Trigger'); + + this.switchService.getCharacteristic(Characteristic.On) + .on('get', callback => callback(null, false)) + .on('set', (value, callback) => { + // Allow manual trigger + if (value) { + this.log.info('Adhan Triggered Manually'); + // We don't play audio on manual trigger by default unless we want to test? + // Let's just set timeout to turn off. + setTimeout(() => { + this.switchService.updateCharacteristic(Characteristic.On, false); + }, 5000); + } + callback(); + }); + + accessory.on('identify', (paired, callback) => { + this.log.info('Identifying Adhan Clock'); + callback(); + }); + } + + schedulePrayers() { + this.log.info('Calculating prayer times for today...'); + const coordinates = new adhan.Coordinates(this.lat, this.lon); + const date = new Date(); + const params = this.calculationMethod(); + + const prayerTimes = new adhan.PrayerTimes(coordinates, date, params); + + const prayers = ['fajr', 'dhuhr', 'asr', 'maghrib', 'isha']; + const now = new Date(); + + prayers.forEach(prayer => { + const time = prayerTimes[prayer]; + if (time > now) { + this.log.info(`Scheduling ${prayer} at ${time.toLocaleTimeString()}`); + schedule.scheduleJob(time, () => { + this.triggerAdhan(prayer); + }); + } + }); + } + + triggerAdhan(prayerName) { + this.log.info(`Time for ${prayerName}! Triggering Adhan...`); + + // Turn on the switch + if (this.switchService) { + this.switchService.updateCharacteristic(Characteristic.On, true); + // Turn off after 5 minutes (standard adhan length approx) + setTimeout(() => { + this.switchService.updateCharacteristic(Characteristic.On, false); + }, 5 * 60 * 1000); + } + + // Play Audio if enabled + if (this.playAudio) { + this.playAdhanAudio(prayerName); + } + } + + playAdhanAudio(prayerName) { + const isFajr = prayerName === 'fajr'; + const file = this.getRandomAdhanFile(isFajr); + + if (!file) { + this.log.warn('No audio file found to play.'); + return; + } + + const volume = 100; // Can be parameterized + const cmd = `mpv --audio-device=${this.audioDevice} --volume=${volume} --no-video "${file}"`; + + this.log.info(`Playing: ${cmd}`); + exec(cmd, (error, stdout, stderr) => { + if (error) { + this.log.error(`Error playing audio: ${error.message}`); + return; + } + if (stderr) this.log.debug(`mpv stderr: ${stderr}`); + + // Play Dua after Adhan + const duaFile = path.join(this.mediaPath, 'after-adhan-dua.mp3'); + if (fs.existsSync(duaFile)) { + const duaCmd = `mpv --audio-device=${this.audioDevice} --volume=${volume} --no-video "${duaFile}"`; + this.log.info('Playing Dua...'); + exec(duaCmd); + } + }); + } + + getRandomAdhanFile(isFajr) { + try { + const files = fs.readdirSync(this.mediaPath).filter(f => f.endsWith('.mp3') && f.startsWith('Adhan')); + if (files.length === 0) return null; + + const fajrFiles = files.filter(f => f.toLowerCase().includes('fajr')); + const regularFiles = files.filter(f => !f.toLowerCase().includes('fajr')); + + if (isFajr) { + if (fajrFiles.length > 0) return path.join(this.mediaPath, fajrFiles[Math.floor(Math.random() * fajrFiles.length)]); + if (regularFiles.length > 0) return path.join(this.mediaPath, regularFiles[Math.floor(Math.random() * regularFiles.length)]); + } else { + if (regularFiles.length > 0) return path.join(this.mediaPath, regularFiles[Math.floor(Math.random() * regularFiles.length)]); + if (fajrFiles.length > 0) return path.join(this.mediaPath, fajrFiles[Math.floor(Math.random() * fajrFiles.length)]); + } + return path.join(this.mediaPath, files[0]); // Fallback + } catch (e) { + this.log.error(`Error accessing media directory: ${e.message}`); + return null; + } + } +} diff --git a/homebridge-plugin/package.json b/homebridge-plugin/package.json new file mode 100644 index 0000000..6ce2e35 --- /dev/null +++ b/homebridge-plugin/package.json @@ -0,0 +1,23 @@ +{ + "name": "homebridge-rasppi-adhan", + "version": "1.0.0", + "description": "Homebridge plugin to trigger Adhan and play audio on Raspberry Pi", + "keywords": [ + "homebridge-plugin", + "adhan", + "prayer", + "islam" + ], + "engines": { + "node": ">=14.18.1", + "homebridge": ">=1.4.0" + }, + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "adhan": "^4.4.4", + "node-schedule": "^2.1.1" + } +}