├── coffee_machine.JPG ├── sounds └── coffee-sound.m4a ├── requirements.txt ├── .env.example ├── coffee_machine_service.service ├── coffee_machine_service.timer ├── makefile ├── .gitignore ├── detect_sound.py └── README.md /coffee_machine.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/dashboard-coffee-listener/master/coffee_machine.JPG -------------------------------------------------------------------------------- /sounds/coffee-sound.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/dashboard-coffee-listener/master/sounds/coffee-sound.m4a -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | librosa==0.8.0 2 | sounddevice==0.4.2 3 | numba==0.50.0 4 | requests==2.26.0 5 | python-dotenv==0.19.1 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | COFFEE_AUDIO_PATH=/home/pi/dashboard-coffee-listener/sounds/coffee-sound.m4a 2 | SD_DEFAULT_DEVICE=2 3 | API_URL=https://dashboard.spatie.be/api/coffee 4 | API_KEY= -------------------------------------------------------------------------------- /coffee_machine_service.service: -------------------------------------------------------------------------------- 1 | # systemd unit file for the Python Service 2 | [Unit] 3 | 4 | # Human readable name of the unit 5 | Description=Coffee machine counter 6 | 7 | 8 | [Service] 9 | # TODO: change user 10 | User=pi 11 | # Command to execute when the service is started 12 | # TODO: change the path to the project directory 13 | WorkingDirectory= /home/pi/dashboard-coffee-listener 14 | ExecStart=make run 15 | 16 | # Automatically restart the service 17 | Restart=always 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /coffee_machine_service.timer: -------------------------------------------------------------------------------- 1 | # systemd unit file for the systemd Service 2 | [Unit] 3 | 4 | # Human readable name of the unit 5 | Description=Coffee machine counter timer 6 | 7 | [Timer] 8 | # Run service everyday at 7 AM 9 | OnCalendar=Mon..Fri 07:00 10 | 11 | # If we need to run it in the middle of the day (e.g. power outage) then also run it 12 | Persistent=true 13 | 14 | Unit=coffee_machine_service.service 15 | 16 | [Install] 17 | 18 | # Tell systemd to automatically start this service when the system boots 19 | # (assuming the service is enabled) 20 | WantedBy=timers.target 21 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | ############################################################### 4 | # COMMANDS # 5 | ############################################################### 6 | install: ## installing dependencies for ARM architectures for all x86 architectures 7 | @echo ">>> installing dependencies for x86 architectures" 8 | pip3 install -r requirements.txt 9 | 10 | install-arm: ## installing dependencies for ARM architectures like Raspberry Pi 11 | @echo ">>> installing dependencies for ARM architectures like Raspberry Pi" 12 | sudo apt install -y libatlas-base-dev libportaudio2 llvm-9 13 | LLVM_CONFIG=llvm-config-9 pip3 install llvmlite==0.33 14 | LLVM_CONFIG=llvm-config-9 pip3 install -r requirements.txt 15 | 16 | run: ## Start listening the environment to detect coffee sound 17 | @echo ">>> generating features" 18 | python3 detect_sound.py 19 | 20 | run-systemctl: ## Start listening the environment with systemctl (auto-restart if fails) 21 | @echo ">>> generating features" 22 | sudo cp coffee_machine_service.service /etc/systemd/system/coffee_machine_service.service 23 | sudo cp coffee_machine_service.timer /etc/systemd/system/coffee_machine_service.timer 24 | sudo systemctl enable coffee_machine_service.service 25 | sudo systemctl enable coffee_machine_service.timer 26 | sudo systemctl daemon-reload 27 | sudo systemctl restart coffee_machine_service.timer 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | coffee_machine.logs 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | .env 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | -------------------------------------------------------------------------------- /detect_sound.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from datetime import datetime 3 | import time 4 | from pathlib import Path 5 | from typing import Union 6 | import os 7 | import logging 8 | import requests 9 | import librosa 10 | import numpy as np 11 | import sounddevice as sd 12 | from scipy.spatial import distance 13 | from dotenv import load_dotenv 14 | 15 | load_dotenv() 16 | 17 | logging.basicConfig( 18 | filename="coffee_machine.logs", 19 | filemode='a', 20 | format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', 21 | level=20 # INFO 22 | ) 23 | logger = logging.getLogger() 24 | 25 | 26 | class AudioHandler: 27 | def __init__(self) -> None: 28 | self.DIST_THRESHOLD = 85 29 | self.sr = 44100 30 | self.sec = 0.7 31 | self.coffee_machine_mfcc, _ = self._set_coffee_machine_features() 32 | sd.default.device = os.getenv("SD_DEFAULT_DEVICE") 33 | 34 | def start_detection(self) -> None: 35 | """ 36 | Start listening the environment and if the euclidean distance 3 times less than the threshold 37 | then count it as coffee machine sound 38 | """ 39 | logger.info("Listening...") 40 | logger.info(f"sampling rate: {self.sr}") 41 | logger.info(sd.query_devices()) 42 | 43 | d = deque([500, 500, 500], 3) 44 | timeout = 12 * 60 * 60 # [seconds] 45 | timeout_start = time.time() 46 | 47 | while time.time() < timeout_start + timeout: 48 | sound_record = sd.rec( 49 | int(self.sec * self.sr), 50 | samplerate=self.sr, 51 | channels=1, 52 | dtype="float32", 53 | blocking=True, 54 | ).flatten() 55 | 56 | mfcc_features = self._compute_mean_mfcc( 57 | sound_record, self.sr 58 | ) 59 | score = distance.euclidean(self.coffee_machine_mfcc, mfcc_features) 60 | d.appendleft(score) 61 | if np.max(d) < self.DIST_THRESHOLD: 62 | logger.info("coffee machine") 63 | logger.info(d) 64 | self.send_api_request() 65 | time.sleep(43) 66 | d = deque([500, 500, 500], 3) 67 | logger.info("start listening again..") 68 | # print(d) 69 | logger.info("End of the day, code run successfully ..") 70 | 71 | def _set_coffee_machine_features(self) -> Union[np.array, int]: 72 | coffee_machine_audio, sr = librosa.load( 73 | os.getenv("COFFEE_AUDIO_PATH"), 74 | sr=self.sr 75 | ) 76 | coffee_machine_audio = coffee_machine_audio[:int(self.sec * self.sr)] 77 | coffee_machine_mfcc = self._compute_mean_mfcc(coffee_machine_audio, sr) 78 | return coffee_machine_mfcc, sr 79 | 80 | @staticmethod 81 | def _compute_mean_mfcc(audio, sr, dtype="float32"): 82 | mfcc_features = librosa.feature.mfcc(audio, sr=sr, dtype=dtype, n_mfcc=20) 83 | return np.mean(mfcc_features, axis=1) 84 | 85 | @staticmethod 86 | def send_api_request(): 87 | logger.info("sending API request") 88 | requests.post(url = os.getenv("API_URL"), data = {'api_key': os.getenv("API_KEY")}) 89 | 90 | if __name__ == '__main__': 91 | AudioHandler().start_detection() 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [](https://supportukrainenow.org) 3 | 4 | # Dashboard Coffee Listener 5 | 6 | This is a fork from [Dataroot's Fresh-Coffee-Listener repo](https://github.com/datarootsio/fresh-coffee-listener). It includes the following changes: 7 | 8 | - added python-dotenv (see `.env.example`) 9 | - removed the PostreSQL integration 10 | - added a simple API request to handle the coffee event (url in `.env`) 11 | 12 | *Below is the original readme by Dataroot.* 13 | 14 | ----------------------------------------- 15 | 16 | A typical datarootsian consumes high-quality fresh coffee in their office environment. The board of dataroots had 17 | a very critical decision by the end of 2021-Q2 regarding coffee consumption. From now on, the total number of coffee 18 | consumption stats have to be audited live via listening to the coffee grinder sound in Raspberry Pi, because why not? 19 | Check stats from [here](https://app.cumul.io/s/coffeeometer-lejpayjm3rdjgtiu). 20 | 21 | ## Overall flow to collect coffee machine stats 22 | 1. Relocate the Raspberry Pi microphone just next to the coffee machine 23 | 2. Listen and record environment sound at every 0.7 seconds 24 | 3. Compare the recorded environment sound with the original coffee grinder sound and measure the 25 | Euclidean distance 26 | 4. If the distance is less than a threshold it means that the coffee machine has been started and a 27 | datarootsian is grabbing a coffee 28 | 5. Connect to DB and send timestamp, office name, and serving type to the DB in case an event is detected ( 29 | E.g. 2021-08-04 18:03:57, Leuven, coffee 30 | ) 31 | 32 | ## Raspberry Pi Setup 33 | ![](coffee_machine.JPG "coffee machine") 34 | 35 | 1. **Hardware**: Raspberry Pi 3b 36 | 2. **Microphone**: External USB microphone (doesn't have to be a high-quality one). We also bought a 37 | microphone with an audio jack but apparently, the Raspberry Pi audio jack doesn't have an input. So, don't do the same mistake and just go for the USB one :) 38 | 3. **OS**: Raspbian OS 39 | 4. **Python Version**: Python 3.7.3. We used the default Python3 since we don't have any other python projects in the same Raspberry Pi. You may also create a virtual environment. 40 | 41 | ## Detecting the Coffee Machine Sound 42 | 1. In the `sounds` folder, there is a `coffee-sound.m4a` file, which is the recording of the coffee machine grinding sound for 1 sec. You need to replace this recording with your coffee machine recording. It is very important to note that record the coffee machine sound with the external microphone that you will use in Raspberry Pi to have a much better performance. 43 | 2. When we run `detect_sound.py`, it first reads the `coffee-sound.m4a` file and extracts its [MFCC](https://en.wikipedia.org/wiki/Mel-frequency_cepstrum) features. 44 | By default, it extracts 20 MFCC features. Let's call these features `original sound features` 45 | 3. The external microphone starts listening to the environment for about 0.7 seconds with a 44100 sample rate. Note 46 | that the 44100 sample rate is quite overkilling but Raspberry Pi doesn't support lower sample rates out of the box. 47 | To make it simple we prefer to use a 44100 sample rate. 48 | 4. After each record, we also extract 20 `MFCC` features and compute the [Euclidean Distance](https://en.wikipedia.org/wiki/Euclidean_distance#:~:text=In%20mathematics%2C%20the%20Euclidean%20distance,being%20called%20the%20Pythagorean%20distance.) 49 | between the `original sound features` and `recorded sound features`. 50 | 5. We append the `Euclidean Distance` to a [python deque object](https://docs.python.org/3/library/collections.html#collections.deque) 51 | having size 3. 52 | 6. If the maximum distance in this deque is less than `self.DIST_THRESHOLD = 85`, then it means that there is a 53 | coffee machine usage attempt. Feel free to play with this threshold based on your requirements. You can simply 54 | comment out `line 66` of `detect_sound.py` to print the deque object and try to select the best threshold. We prefer 55 | to check 3 events (i.e having deque size=3) subsequently to make it more resilient to similar sounds. 56 | 7. Go back to step 3, if the elapsed time is < 12 hours. (Assuming that the code will run at 7 AM and ends at 7 PM 57 | since no one will be at the office after 7 PM) 58 | 8. Exit 59 | 60 | ## Scheduling the coffee listening job 61 | We use a systemd service and timer to schedule the running of `detect_sound.py`. Please check `coffee_machine_service.service` 62 | and `coffee_machine_service.timer` files. This timer is enabled in the `makefile`. It means that even if you reboot your 63 | machine, the app will still work. 64 | 65 | #### coffee_machine_service.service 66 | In this file, you need to set the correct `USER` and `WorkingDirectory`. In our case, our settings are; 67 | 68 | ```shell 69 | User=pi 70 | WorkingDirectory= /home/pi/dashboard-coffee-listener 71 | ``` 72 | To make the app robust, we set `Restart=on-failure`. So, the service will restart if something goes wrong in the app. (E.g power outage, someone plugs out the microphone and plug in again, etc.). This service will trigger `make run` 73 | the command that we will cover in the following sections. 74 | 75 | #### coffee_machine_service.timer 76 | The purpose of this file is to schedule the starting time of the app. As you see in; 77 | ```shell 78 | OnCalendar=Mon..Fri 07:00 79 | ``` 80 | It means that the app will work every weekday at 7 AM. Each run will take 7 hours. So, the app will complete 81 | listening at 7 PM. 82 | 83 | ## Deploying Fresh-Coffee-Listener app 84 | 1. **Installing dependencies**: If you are using an ARM-based device like Raspberry-Pi run 85 | ```shell 86 | make install-arm 87 | ``` 88 | For other devices having X84 architecture, you can simply run 89 | ```shell 90 | make install 91 | ``` 92 | 93 | 2. **Set Variables in makefile** 94 | - `COFFEE_AUDIO_PATH`: The absolute path of the original coffee machine sound (E.g. `/home/pi/coffee-machine-monitoring/sounds/coffee-sound.m4a`) 95 | - `SD_DEFAULT_DEVICE`: It is an integer value represents the sounddevice input device number. To find your external device number, run 96 | `python3 -m sounddevice` and you will see something like below; 97 | ```shell 98 | 0 bcm2835 HDMI 1: - (hw:0,0), ALSA (0 in, 8 out) 99 | 1 bcm2835 Headphones: - (hw:1,0), ALSA (0 in, 8 out) 100 | 2 USB PnP Sound Device: Audio (hw:2,0), ALSA (1 in, 0 out) 101 | 3 sysdefault, ALSA (0 in, 128 out) 102 | 4 lavrate, ALSA (0 in, 128 out) 103 | 5 samplerate, ALSA (0 in, 128 out) 104 | 6 speexrate, ALSA (0 in, 128 out) 105 | 7 pulse, ALSA (32 in, 32 out) 106 | 8 upmix, ALSA (0 in, 8 out) 107 | 9 vdownmix, ALSA (0 in, 6 out) 108 | 10 dmix, ALSA (0 in, 2 out) 109 | * 11 default, ALSA (32 in, 32 out) 110 | ``` 111 | It means that our default device is `2` since the name of the external device is `USB PnP Sound Device`. So, we will 112 | set it as `SD_DEFAULT_DEVICE=2` in our case. 113 | 114 | 3. **Sanity check**: Run `make run` to see if the app works as expected. You can also have a coffee to test whether it captures 115 | the coffee machine sound. 116 | 117 | 4. **Enabling systemd commands to schedule jobs**: After configuring `coffee_machine_service.service` and 118 | `coffee_machine_service.timer` based on your preferences, as shown above, run to fully deploy the app; 119 | ```shell 120 | make run-systemctl 121 | ``` 122 | 5. Check the `coffee_machine.logs` file under the project root directory, if the app works as expected 123 | 6. Check service and timer status with the following commands 124 | ```shell 125 | systemctl status coffee_machine_service.service 126 | ``` 127 | and 128 | ```shell 129 | systemctl status coffee_machine_service.timer 130 | ``` 131 | 132 | ## Having Questions / Improvements ? 133 | Feel free to create an issue and we will do our best to help your coffee machine as well :) 134 | --------------------------------------------------------------------------------