Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
76 changes: 76 additions & 0 deletions homebridge-plugin/README.md
Original file line number Diff line number Diff line change
@@ -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
```
58 changes: 58 additions & 0 deletions homebridge-plugin/config.schema.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
}
182 changes: 182 additions & 0 deletions homebridge-plugin/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
23 changes: 23 additions & 0 deletions homebridge-plugin/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}