19H — Réveil 2.0
Par admin le mardi, février 5 2019, 12:00 - 2018-2019 - Lien permanent
Ce projet a été réalisé par David Kalajdzic.
1. Introduction
Ce projet a un objectif simple : transformer les pénibles réveils en un pure instant de plaisir.
En effet, les réveils de nos jours sonnent de manière bruyante et cela n’est pas une bonne façon de commencer une journée. Il faut donc repenser cet objet. Son fonctionnement, ses fonctionnalités, etc. Un réveil intelligent : un réveil 2.0
Il devra être capable de décider par lui même l’heure du réveil (en prenant en considération les horaires, les professeurs absents et l’agenda Google pour les jours de congé). Il faut aussi redéfinir la manière de réveiller. En effet le bouton « snooze » devra disparaître et immédiatement réveiller l’utilisateur (c’est en effet une grande source d’arrivée tardives le lundi matin, les fameuses « juste 5 Minutes de plus ») avec de la musique notamment. Ensuite l’utilisateur sera appelé (avec une voix) à résoudre un petit défi qui le maintiendra éveillé quelques minutes. Par exemple l'écran dit à l'utilisateur de bouger le joystick vers la droite, ainsi l'utilisateur exécute ce mouvement.
N’est-ce pas une meilleure façon de se réveiller ?
L’enjeu de ce projet sera la conception de ce réveil 2.0 du point de vue physique et du point de vue logiciel. Ces deux éléments devront fonctionner main dans la main.
Voici une première idée de l'objectif à atteindre
Figure 1 - Croquis initial du réveil 2.0
2. Matériel et méthodes
2.1 Matériel
2.2 Méthode
Comme tout bon projet informatique, il faut procéder par étapes.
2.2.1 Processus de réveil
Le processus de réveil comprends la partie où le réveil sonne jusqu'à son extinction. Ce processus doit donc lancer la musique et afficher le petit jeu expliqué en introduction (avec le Joystick...) Ainsi après avoir terminé ce jeu la musique s'éteindra et l'écran aussi.
2.2.1.1 Spotify
Il faut pouvoir écouter de la musique au réveil. Partie hardware c'est très simple : simplement brancher un petit haut parleur sur la prise jack du Raspberry.
Par contre partie software c'est plus long :
Spotify ne fournis pas directement un moyen simple de faire cela. Il faut donc mettre en place un serveur MPD (Music Player Daemon). Mopidy en est un. Ils servent à écouter du contenu audio de diverses sources (fichiers, urls, etc.) et ils sont contrôlables par des clients MPD.
2.2.1.2 Mise en place de Mopidy
Ajouter la clé d'archivage GPG :
$ sudo wget https://apt.mopidy.com/mopidy.gpg $ sudo apt-key add mopidy.gpg
Installer Mopidy
$ sudo apt-get install mopidy
Ensuite il faut ajouter une extension à ce serveur : l'extension Spotify pour Mopidy :
$ sudo apt-get install mopidy-spotify
Il faut avoir un compte premium Spotify et créer les tokens/ids à l'adresse https://developer.spotify.com/dashboard/applications
Désormais il faut connecter ce compte au serveur :
$ cd /home/pi/.config/modipy $ nano modipy.conf
Ajouter à la fin du fichier :
[spotify] username = username ici password = mot de passe ici client_id = client id ici client_secret = secret ici
Pour lancer le serveur MPD, simplement :
$ mopidy
2.2.1.3 Contrôle en Python
Il existe plusieurs types de clients MPD certains sont des interfaces graphiques, etc. Il existe mpd2 une bibliothèque pour Python :
$ pip install python-mpd2
Et ensuite j'ai créé un objet qui va s'occuper de cette tâche:
from mpd import MPDClient class Player(object): """C'est notre player""" def __init__(self): self._client = MPDClient() self._client.timeout = 10 self._client.idletimeout = None self._client.connect('localhost', 6600) def startMusic(self): for uri in self._getSongsURIs(): self._client.add(str(uri)) self._client.random(1) self._client.play() def stopMusic(self): self._client.pause() self._client.clear() def _getSongsURIs(self): file = open('songsToWakeUp.txt', 'r') URIs = file.read().split('\n') return URIs
Le fichier songsToWakeUp.txt a une petite liste de chansons (les URIs de Spotify) qui seront envoyées au serveur Mopidy et jouées aléatoirement.
2.2.1.4 Écran LCD
Figure 2 - Schéma de connections de l'écran
Après avoir fait toutes ces connections, pour pouvoir commander cet écran LCD 16x2 nous devons installer :
$ git clone https://github.com/adafruit/Adafruit_Python_CharLCD.git $ cd ./Adafruit_Python_CharLCD $ sudo python setup.py install
Puis on crée la classe Display dont on peut commander le fonctionnement avec l'interface (les méthodes public) :
# -*- coding: utf-8 -*- import Adafruit_CharLCD as LCD import datetime class Display(object): def __init__(self): lcd_rs = 25 lcd_en = 24 lcd_d4 = 23 lcd_d5 = 17 lcd_d6 = 18 lcd_d7 = 22 lcd_backlight = 4 lcd_columns = 16 lcd_rows = 2 self._lcd = LCD.Adafruit_CharLCD(lcd_rs, lcd_en, lcd_d4, lcd_d5, lcd_d6, lcd_d7, lcd_columns, lcd_rows, lcd_backlight) self.turnOffBacklight() def display(self, l1, l2): self._lcd.clear() self._lcd.message(l1+'\n'+l2) def displayCurrentTime(self): time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') self.display(' Reveil 2.0', time) def turnOnBacklight(self): self._lcd.set_backlight(1) def turnOffBacklight(self): self._lcd.set_backlight(0)
2.2.1.5 Joystick
Le joystick est en fait une combinaison de deux petits potentiomètres. C'est donc des données analogiques que le Raspberry Pi devra lire. Par défaut il ne peut pas faire cela : il faut un chip qui fait ce passage de analogique au digital. Il s'appelle le MCP3008. Voici donc les connections à faire :
Figure 3 - Schéma des connections : MCP3008 et Joystick
La classe Joystick ci-dessous s'occupe de lire la position du Joystick et à l'aide de la méthode readPosition() nous pouvons connaître la position du Joystick.
import Adafruit_GPIO.SPI as SPI import Adafruit_MCP3008 class Joystick(object): def __init__(self): SPI_PORT = 0 SPI_DEVICE = 0 self._mcp = Adafruit_MCP3008.MCP3008(spi=SPI.SpiDev(SPI_PORT, SPI_DEVICE)) def readPosition(self): # Read all the ADC channel values in a list. adcValues = [0,0,0,0,0,0,0,0] for i in range(8): adcValues[i] = self._mcp.read_adc(i) return self._devine(adcValues) def _devine(self, adcValues): if adcValues[1] > 900: return 3 #gauche if adcValues[1] < 10: return 1 #droite if adcValues[2] > 900: return 2 #bas if adcValues[2] < 10: return 0 #haut return -1 # initial
2.2.1.6 Led Multi-Colore
import RPi.GPIO as GPIO import time as time class Led(object): def __init__(self): GPIO.setmode(GPIO.BCM) GPIO.setup(12, GPIO.OUT)#red GPIO.setup(27, GPIO.OUT)#green GPIO.setup(13, GPIO.OUT)#blue self._setColor(0, 0, 0) def _setColor(r, g, b): gpioR = GPIO.HIGH if r == 1 else GPIO.LOW gpioG = GPIO.HIGH if g == 1 else GPIO.LOW gpioB = GPIO.HIGH if b == 1 else GPIO.LOW GPIO.output(12, gpioR) GPIO.output(27, gpioG) GPIO.output(13, gpioB) def blinkRed(self): self._setColor(1, 0, 0) time.sleep(0.6) self._setColor(0, 0, 0) def blinkBlue(self): self._setColor(0, 0, 1) time.sleep(0.6) self._setColor(0, 0, 0)
La led est réglable en RGB avec des valeurs de 1 ou 0. (nous n'avons pas besoin des ~16 millions de couleurs différentes).
2.2.1.7 Ringtone
Nous avons désormais toutes les pièces pour créer le processus de réveil dit auparavant. Ringtone est exactement celui qui permet de faire cela :
import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) import time import random import os from WakeUpCalculator import WakeUpCalculator from Joystick import Joystick from Display import Display from Player import Player from Led import Led class Ringtone(object): '''Object that starts song, and with a game to turn it off''' def __init__(self): self._player = Player() self._display = Display() self._joystick = Joystick() self._led = Led() self._desiredMove = 0 # by default first move to do is "Up" self._i = 0 # nbr exercice def startWakingUp(self): self._startedGame() previousValue = -1 while True: current = self._joystick.readPosition() if previousValue != current: previousValue = current if current != -1: # here we finnally have user's move. if current == self._desiredMove: self._i+=1 if self._i == 20: self._finishedGame() break else : self._goodMoveDone() else : self._badMoveDone() time.sleep(0.1) def _goodMoveDone(self): self._led.blinkBlue() self._desiredMove = random.randint(0, 3) direction = self._getMovesWord(self._desiredMove) self._display.display(str(self._i)+'/20 fait', 'Bougez '+direction) def _badMoveDone(self): self._led.blinkRed() self._display.display('Erreur bad direction', 'Bougez '+self._getMovesWord(self._desiredMove)) def _getMovesWord(self, moveNbr): switcher = { 0:'Haut', 1:'Droite', 2:'Bas', 3:'Gauche' } return switcher.get(moveNbr, 'error') def _startedGame(self): os.system("espeak 'Hello, come play to turn off the alarm clock'") self._player.startMusic() self._display.display('Joue ce jeu :)', 'Bougez Haut') self._display.turnOnBacklight() def _finishedGame(self): self._player.stopMusic() self._display.display('Bonjour !', 'Bonne journee') time.sleep(1.5) self._display.turnOffBacklight()
La méthode importante est startWakingUp(). En effet celle-ci appelle une méthode startedGame() et ensuite lit la position du Joystick en continu. S'il y a eu un mouvement de Joystick, Ringtone va comparer le mouvement avec le mouvement attendu pour choisir entre : afficher l'erreur et demander de refaire le mouvement ou tirer au hasard un prochain mouvement attendu et l'afficher. L'utilisateur devra réussir 20 fois pour que le programme "sorte" de startWakingUp.
2.2.2 Déterminer l'heure du réveil
Ce réveil autonomie ne nécessite aucun réglage : il est entièrement autonome. Il faut donc qu'il détermine l'heure du réveil en fonction des horaires habituelles, des professeurs absents (ecran.gyre.ch) et de l'agenda Google dans lequel les jours de congés seront écrits avec un #CONGE.
2.2.2.1 Horaires habituelles
Après maintes réflexions voici la manière finale : Il faut stocker d'une manière ou d'une autre les horaires avec les professeurs du cours. De plus nous devons connaître les heures de début de cour de chaque période.
Alors on commence par créer une classe Periode qui a comme attributs le nom du professeur et l'heure du début de période. Une méthode intéressante dans cet objet est "isSameTeacherAs", il permettra de tester (depuis l'extérieur) si le nom d'un professeur est associé au nom du professeur qui donne le cour à cette période-ci.
from difflib import SequenceMatcher from datetime import time class Periode(object): '''teacher & hour of a lesson''' def __init__(self, teacher, indexInDay): self._teacher = teacher self._beginTime = self._indexOfDayToBeginTime(indexInDay) def _indexOfDayToBeginTime(self, indexInDay): switcher = { 0: time(8, 15, 0), 1: time(9, 10, 0), 2: time(10, 15, 0), 3: time(11, 10, 0), } return switcher.get(indexInDay, "error") def isSameTeacherAs(self, textTeacher): '''test if they are the same teacher or not''' similarity = SequenceMatcher(a=self._teacher,b=textTeacher).ratio() return True if similarity > 0.90 else False def getBeginTime(self): return self._beginTime
Finalement ce que l'on veut faire c'est d'instance un objet Horaire et de pouvoir lui demander de "getTodayPeriodes". La classe Horaire est :
from Periode import Periode import datetime class Horaire(object): ''' horaires normales ''' def __init__(self): lines = open('horaire.txt').readlines() monday = self._initPeriodes(lines[0:4]) tuesday = self._initPeriodes(lines[4:8]) wednesday = self._initPeriodes(lines[8:12]) thursday = self._initPeriodes(lines[12:16]) friday = self._initPeriodes(lines[16:20]) self._horaire = [monday, tuesday, wednesday, thursday, friday] def _initPeriodes(self, lstProf) : '''from a list of names, create a list of object Periode. -> for a day(to 12h00)''' periodes = [] for i, name in enumerate(lstProf): name = name.replace('\n', '')# undesired character periodes.append(Periode(name, i)) return periodes def getTodayPeriodes(self): nbrDay = datetime.datetime.today().weekday() # 0 : monday, 6 : sunday return self._horaire[nbrDay]
Le fichier horaire.txt est de cette structure :
NomProfesseur 1ère période lundi NomProfesseur 2ème période lundi NomProfesseur 3ème période lundi NomProfesseur 4ème période lundi etc. NomProfesseur 4ème période vendredi
avec juste le nom des professeurs ligne après ligne. Dont l'ordre défini la i-ème période de la journée. Notez que j'ai choisi d'écrire uniquement les 4 périodes du matin (en effet le réveil ne va pas sonner l'après-midi :) )
2.2.2.2 Professeurs absents
Le site internet ecran.gyre.ch informe aux élèves des professeurs absents. Le but ici est donc d'extraire cette information et de la modéliser avec des objets pour la rendre manipulable.
import requests from datetime import datetime, date from Absence import Absence class EcranGyre(object): ''' ecran.gyre.ch ''' def __init__(self): file = open('password.txt','r') self._username = file.readline()[:-1] self._password = file.readline()[:-1] def getTodayAbsences(self): todayList = [] todayDate = datetime.today().isoformat() for a in self._getAbsences(): if a.getDate() == todayDate : todayList.append(a) return todayList def _getAbsences(self): self._gyreMessage = self._refreshFromWebsite() lines = self._gyreMessage.split('\n') absenceList = [] for line in lines: if line.strip():# select only lines that have somethin in it... absenceList.append(Absence(line)) return absenceList def _refreshFromWebsite(self): session = requests.Session() response = session.get('http://ecran.gyre.ch/login?u='+self._username+'&p='+self._password) r = session.get('https://ecran.gyre.ch/sources/absences') r.content.decode('ISO-8859-1') return r.text.replace('<br>', '').encode('utf-8')
le fichier password.txt contient les identifiants pour accéder aux informations du site internet (la première ligne c'est l'email, et la deuxième c'est le mot de passe) On voit que cet EcranGyre va simplement s'occuper de la partie requête et instance des objets Absence qui s'occupent quant à eux d'extraire l'information. Voici un exemple de contenu reçu :
Mme Nicole Kaech (allemand), absente le mercredi 16 janvier 2019, le matin. Mme Valérie Roten (sociologie des médias), absente du mercredi 16 au jeudi 17 janvier 2019. Mme Géraldine Voelke-Viscardi (latin et histoire de l'art), absente le mercredi 16 janvier 2019. M. Manuel Zenger (biologie), absent le mercredi 16 janvier 2019, l'après-midi.
J'ai remarqué que les cas simples sont écrits sous cette forme :
#nomProfesseur (branches), absent-e #date, le matin/apèrs-midi/toute la journée
Voici comme Absence se débrouille avec ça :
import re import dateparser class Absence(object): '''Objet qui formatte a partir de la ligne du site web un objet avec des variables accessibles''' def __init__(self, text): self._teacher = self._extractTeacher(text) self._dayTxt = self._extractDayTxt(text) self._dayMoment = self._extractMoment(text) # matin 0/après-midi 1/toute la journée 2 def _extractTeacher(self, text): result = re.search('(.*) \(', text) return result.group(1) def _extractDayTxt(self, text): result = re.search(' le(.*)', text) if result != None : puredatefrench = result.group(1).split(',', 1)[0] return puredatefrench return None def _extractMoment(self, text): if text.find('matin') != -1: return 0 elif text.find('midi') != -1: return 1 return 2 def getDate(self): if self._dayTxt != None : return dateparser.parse(self._dayTxt).date() return ''
La seule méthode dite publique est getDate(). Il va convertir dayTxt (de la forme "mercredi 16 janvier 2019") la date en version "humaine" en date ordinateur/"python". La bibliothèque dateparser s'occupe de ça.
$ pip install dateparser
2.2.2.3 Jours de congé
Personnellement j'inscrit les jours de congé sur mon agenda Google. Il serait génial que ce réveil puisse accéder à cet agenda (ça éviterait que l'on se lève pour aller en cours et se rendre compte qu'il y avait congé :) )
GoogleAgenda s'occupe de ça :
from __future__ import print_function import pickle import os.path from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request from datetime import datetime from datetime import date class GoogleAgenda(object): def __init__(self): self._service = build('calendar', 'v3', credentials=self._getCredentials()) def _getCredentials(self): '''le code dans cette methode a ete fait par Google. (https://developers.google.com/calendar/quickstart/python)''' creds = None # The file token.pickle stores the user's access and refresh tokens, and is # created automatically when the authorization flow completes for the first # time. if os.path.exists('token.pickle'): with open('token.pickle', 'rb') as token: creds = pickle.load(token) # If there are no (valid) credentials available, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file('credentials.json', ['https://www.googleapis.com/auth/calendar.readonly']) creds = flow.run_local_server() # Save the credentials for the next run with open('token.pickle', 'wb') as token: pickle.dump(creds, token) return creds def isItFreeDayToday(self): now = datetime.today().isoformat() + 'Z' api_result = self._service.events().list(calendarId='primary', timeMin=now,maxResults=1, singleEvents=True,orderBy='startTime', q='#CONGE').execute() events = api_result.get('items', []) event = events[0] today = date.today().isoformat() nextFreeDay = event['start']['date'] return True if today == nextFreeDay else False
La partie authentification est une méthode privée (elle est par ailleurs faite par google). La méthode intéressante est isItFreeDayToday(). Elle va simplement répondre par un True/False.
Il faut noter que 2 fichiers (token.pickle, credentials.json) sont utiles pour la libraire google-api-python-client. On les installe de cette manière :
$ pip install --upgrade google-api-python-client $ pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
2.2.2.4 WakeUpCalculator
Nous avons construit toutes les pièces indispensables. Les objets Horaire, EcranGyre et GoogleAgenda ont des bonnes interfaces pour nous simplifier la vie ici. WakeUpCalculator va calculer le moment de réveil en fonction des éléments cités ci-dessus mais aussi en prenant compte d'un temps de marge (par ex : 45 minutes de petit-déjeuner + temps de transport) qui est customisable.
from Absence import Absence from EcranGyre import EcranGyre from Horaire import Horaire from GoogleCalendar import GoogleAgenda from datetime import datetime class WakeUpCalculator(object): ''' cet objet détermine l'heure du réveil''' def __init__(self, marginTime, defaultTime): self._marginTime = marginTime self._defaultTime = defaultTime # for week-ends, holidays,... def getTimeToWakeUp(self): agenda = GoogleAgenda() if agenda.isItFreeDayToday(): return self._defaultTime else: return str(self._substractTwoTimes(self._getBeginningTime(), self._marginTime)) def _getBeginningTime(self): horaire = Horaire() ecran = EcranGyre() for p in horaire.getTodayPeriodes(): for absence in ecran.getTodayAbsences(): if p.isSameTeacherAs(absence.getTeacher()) : return p.getBeginTime() # no absent teachers, as usual : return horaire.getTodayPeriodes()[0].getBeginTime() def _substractTwoTimes(self, a, b): format = '%H:%M:%S' return datetime.strptime(str(a), format) - datetime.strptime(str(b), format)
Dans la méthode privée getBeginningTime j'ai mis en place un "mini algorithme" qui va parcourir toutes les périodes du jour commençant par 8h15 jusqu'à 12h00. Ainsi pouvoir les comparer avec chaque professeur absent ce jour-ci. Il continue à parcourir à la prochaine période jusqu'à ce que la période soit reconnue comme absente. À ce moment là, il va dire de se lever pour être à l'heure à la période suivante. Sinon il retourne l'heure de la première période selon l'horaire habituel.
2.2.3 Le MAIN
Le fichier principal que l'on exécute utilise la bibliothèque schedule pour programmer des temps d'exécutions.
$ pip install schedule
#!/usr/bin/python import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) from datetime import time import schedule from WakeUpCalculator import WakeUpCalculator from Ringtone import Ringtone def startRingtone(): print('ringtone') a = Ringtone() a.startWakingUp() return schedule.CancelJob def calculateWakeUpTime(): print('calculate') wakeupcalculator = WakeUpCalculator(time(0, 40, 0), time(9, 0, 0)) waketime = wakeupcalculator.getTimeToWakeUp() schedule.every().day.at(str(waketime)).do(startRingtone) schedule.every().day.at("02:00:00").do(calculateWakeUpTime) while True: schedule.run_pending()
Donc tous les jours à deux heures du matin, le réveil va calculer l'heure du réveil avec WakeUpCalculator. Et ensuite quand l'heure de sonnerie est arrivé, avec Ringtone nous allons exécuter le processus de réveil (musique + jeu).
3. Résultats
Voici en vidéo le moment fatidique du réveil (c'est la seule chose que l'on peut voir, le reste étant "invisible") :
Vidéo 1 - La manière d'éteindre ce réveil
4. Discussion
Le résultat final est très satisfaisant. Ce réveil mérite bien son nom de réveil 2.0. Ce lundi j'ai été réveillé au bon moment (c'est déjà une bonne chose) et la manière dont j'ai été réveillé était très douce et agréable.
Il est important de noter que vu l'ampleur du projet il était difficile de s'imaginer tous ces petits éléments connectés bout à bout : leurs interactions. En effet chaque élément a ses problématiques, ses manières de résoudre, etc. Que ce soit du côté software ou du côté hardware. Le paradigme de programmation objet me plaît beaucoup ici. Car grâce à lui j'ai pu structurer et m'occuper d'une seule chose à la fois. Un deuxième point fort et que l'on différencie entre fonctionnement interne (méthodes privées) et interface (méthodes public). Ce qui fait, par exemple, que l'on encapsule la communication avec le protocole SPI du MCP3008 et que "simplement" on demande à l'objet getJoystickPosition().
J'ai aussi appris à programmer l'orienté objet avec la syntaxe de python. C'était une bonne expérience. Même si l'indentation est très pénible à la longue. Ainsi que de ne pas écrire le type des variables. Faillait être d'autant plus concentré sur ce que l'on écrit.
Le résultat final reste quand même ouvert à une multitude d'amélioration :
- Faire un boîtier
Un boîtier pour contenir toute cette électronique avec notamment un circuit imprimé (une carte mère). Pour la commercialisation par exemple c'est évidemment un point important.
- Avoir un son plus fort
Le problème ici est que ce haut parleur peut faire du son relativement élevé avec un smartphone. Mais ça ne marche pas aussi bien avec le Raspberry. J'imagine qu'il faut un amplificateur (?)
- Avoir une meilleure reconnaissance des professeurs absents
Je pense notamment aux absence-intervalles : ... absent du jeudi 8 décembre au vendredi 9 décembre qui sont des cas non traités
- Avoir un système automatique d'initialisation
Pour que l'on aie pas besoin de devoir manuellement entrer les mots de passes et lancer le Serveur Mopidy. Même si une fois lancé, il n'y a rien besoin de lui dire : il est entièrement autonome.
- Gestion des erreurs.
Sur le schéma vu le premier jour d'informatique (OC) c'était une boîte avec des entrées et des sorties. Mais il y avait aussi les erreurs. Vu l'ampleur de ce projet ça m'aurais pris un temps fou pour gérer toutes les erreurs possibles. C'est pour cela qu'il est tout à fait probable que le programme "crash".
5. Conclusion
Pour conclure, ce projet était un très bon défis pour moi. Je ne connaissais pas la majore partie des éléments. Le hardware tout comme le Software. Il a fallu utiliser plusieurs librairies pour faire marcher ce que je veux. Et il a fallu beaucoup réfléchir sur le programme : qu'il soit le plus "clean" possible. Les interactions entre les différents objets. Sur la modularité du code : que s'il y a le moindre bug, que l'on puisse facilement trouver sa provenance.
Finalement il reste énormément à travailler sur le peaufinage de cet appareil. Il peut être la solution pour résoudre les problèmes de réveil! Tout le monde devrait en avoir un. L'argument marketing serait par exemple : "Ne réglez plus jamais votre réveil, il sait quand vous réveiller". :)
Et s'il y a une chose que je dois retenir de tout ce projet : c'est
RTFM (cherchez sur Google si vous savez pas ce que c'est :))
Oui j'ai du en lire de la doc.
Références
Les principales références :
Serveur Mopidy :
https://en.wikipedia.org/wiki/Music_Player_Daemon
https://docs.mopidy.com/
https://pypi.org/project/Mopidy-Spotify/
https://www.mopidy.com/authenticate/#spotify
https://github.com/mopidy/mopidy-spotify
Contrôle de Mopidy
https://github.com/Mic92/python-mpd2
https://python-mpd2.readthedocs.io/en/latest/
Client terminal : mpc https://www.systutorials.com/docs/linux/man/1-mpc/
Puce MCP3008
https://learn.adafruit.com/raspberry-pi-analog-to-digital-converters/mcp3008
https://learn.adafruit.com/mcp3008-spi-adc/python-circuitpython
https://tutorials-raspberrypi.com/raspberry-pi-joystick-with-mcp3008/
Ecran LCD
https://www.sparkfun.com/products/709
https://www.youtube.com/watch?v=TORjcmXFpn8
https://github.com/adafruit/Adafruit_Python_CharLCD
Google Calendar
https://console.developers.google.com/apis/credentials?project=alarmclock-1548020545993
https://developers.google.com/calendar/quickstart/python
https://developers.google.com/resources/api-libraries/documentation/calendar/v3/python/latest/calendar_v3.events.html#list
Parseur language humain en date
https://dateparser.readthedocs.io/
Scheduler
https://schedule.readthedocs.io/en/stable/