├── .gitignore ├── alexa_audio_device.py ├── alexa_params.py ├── alexa.py ├── alexa_control.py ├── alexa_audio_device_pyaduio.py ├── LICENSE ├── snap └── snapcraft.yaml ├── alexa_auth.py ├── alexa_audio_device_pulse.py ├── README.md ├── alexa_device.py ├── alexa_audio.py └── alexa_http_config.py /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__* 2 | alexa_credentials 3 | snap/parts 4 | snap/prime 5 | snap/stage 6 | snap/*.snap 7 | -------------------------------------------------------------------------------- /alexa_audio_device.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | try: 4 | from alexa_audio_device_pulse import * 5 | except ImportError: 6 | from alexa_audio_device_pyaduio import * 7 | 8 | -------------------------------------------------------------------------------- /alexa_params.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | LOCAL_HOST="alexa.local" 6 | LOCAL_PORT=3000 7 | BASE_URL = "http://" + LOCAL_HOST + ":" + str(LOCAL_PORT) + "/" 8 | DEFAULT_VOICE_THRESHOLD = 0.25 9 | DEFAULT_PRODUCT_ID = "" 10 | DEFAULT_CLIEND_ID = "" 11 | DEFAULT_DEVICE_SERIAL = "" 12 | DEFAULT_CLIENT_SECRET = "" 13 | ALEXA_CREDENTIALS_FILE = os.getenv("SNAP_USER_DATA", ".") + "/alexa_credentials" 14 | -------------------------------------------------------------------------------- /alexa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import alexa_auth 4 | import alexa_audio_device 5 | import logging 6 | 7 | def main(): 8 | logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(message)s', datefmt='%d/%m/%Y %H:%M:%S') 9 | logging.getLogger("requests").setLevel(logging.CRITICAL) 10 | alexa_audio_device.init() 11 | alexa_auth.start() 12 | try: 13 | input("Press Enter to exit...\n") 14 | except KeyboardInterrupt: 15 | pass 16 | alexa_auth.close() 17 | alexa_audio_device.deinit() 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /alexa_control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import json 5 | import logging 6 | import alexa_params 7 | import alexa_device 8 | import alexa_http_config 9 | 10 | alexa = None 11 | 12 | def start(): 13 | global alexa 14 | if alexa is not None: 15 | return 16 | alexa_config = alexa_http_config.load_config() 17 | if alexa_config is not None: 18 | logging.info("Alexa found config.") 19 | alexa = alexa_device.AlexaDevice(alexa_config) 20 | 21 | def close(): 22 | global alexa 23 | if alexa is not None: 24 | alexa.close() 25 | alexa = None 26 | 27 | def main(): 28 | start() 29 | if alexa is not None: 30 | try: 31 | input("Alexa started. Press Enter to exit...\n") 32 | except KeyboardInterrupt: 33 | pass 34 | alexa.close() 35 | 36 | if __name__ == '__main__': 37 | main() 38 | -------------------------------------------------------------------------------- /alexa_audio_device_pyaduio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pyaudio 4 | import math 5 | import struct 6 | 7 | def init(): 8 | pass 9 | 10 | def deinit(): 11 | pass 12 | 13 | class AlexaAudioDevice: 14 | def __init__(self): 15 | self.pa = pyaudio.PyAudio() 16 | self.in_stream = self.pa.open(format=pyaudio.paInt16, channels=1, 17 | rate=16000, input=True) 18 | self.in_stream.start_stream() 19 | self.out_stream = self.pa.open(format=pyaudio.paInt16, channels=1, 20 | rate=16000, output=True) 21 | self.out_stream.start_stream() 22 | 23 | def close(self): 24 | self.in_stream.close() 25 | self.out_stream.close() 26 | self.pa.terminate() 27 | 28 | def write(self, b): 29 | return self.out_stream.write(b) 30 | 31 | def read(self, n): 32 | return self.in_stream.read(n) 33 | 34 | def flush(self): 35 | pass 36 | 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 DeviceHive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: alexa 2 | version: "1.1" 3 | summary: Amazon Alexa snap 4 | description: Add Amazon Alexa to your device 5 | confinement: strict 6 | 7 | apps: 8 | alexa: 9 | command: bin/alexa.py 10 | daemon: simple 11 | plugs: [network, network-bind, pulseaudio] 12 | alexa-run: 13 | command: bin/alexa.py 14 | plugs: [network, network-bind, pulseaudio] 15 | 16 | parts: 17 | python-hyper: 18 | plugin: python 19 | python-version: python3 20 | python-packages: [requests, 'git+https://github.com/moaxey/python-zeroconf', pocketsphinx] 21 | pyalexa: 22 | plugin: dump 23 | source: .. 24 | organize: 25 | alexa.py: bin/alexa.py 26 | alexa_audio.py: bin/alexa_audio.py 27 | alexa_audio_device.py: bin/alexa_audio_device.py 28 | alexa_audio_device_pulse.py: bin/alexa_audio_device_pulse.py 29 | alexa_auth.py: bin/alexa_auth.py 30 | alexa_control.py: bin/alexa_control.py 31 | alexa_device.py: bin/alexa_device.py 32 | alexa_http_config.py: bin/alexa_http_config.py 33 | alexa_params.py: bin/alexa_params.py 34 | stage-packages: 35 | - ffmpeg 36 | - libpulse0 37 | - pulseaudio 38 | build-packages: 39 | - swig 40 | - git 41 | - libpulse-dev 42 | 43 | -------------------------------------------------------------------------------- /alexa_auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import alexa_params 3 | import alexa_control 4 | import alexa_http_config 5 | import socket 6 | import threading 7 | import logging 8 | from zeroconf import raw_input, ServiceInfo, Zeroconf 9 | from http.server import HTTPServer 10 | 11 | localHTTP = None 12 | zeroconf = None 13 | info = None 14 | 15 | def get_local_address(): 16 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 17 | s.connect(("www.amazon.com", 80)) 18 | res = s.getsockname()[0] 19 | s.close() 20 | return res 21 | 22 | def start(): 23 | global localHTTP, zeroconf, info, httpthread 24 | ip = get_local_address() 25 | logging.info("Local IP is " + ip) 26 | 27 | desc = {'version': '0.1'} 28 | info = ServiceInfo("_http._tcp.local.", 29 | "Alexa Device._http._tcp.local.", 30 | socket.inet_aton(ip), alexa_params.LOCAL_PORT, 0, 0, 31 | desc, alexa_params.LOCAL_HOST + ".") 32 | zeroconf = Zeroconf() 33 | zeroconf.registerService(info) 34 | logging.info("Local mDNS is started, domain is " + alexa_params.LOCAL_HOST) 35 | localHTTP = HTTPServer(("", alexa_params.LOCAL_PORT), alexa_http_config.AlexaConfig) 36 | httpthread = threading.Thread(target=localHTTP.serve_forever) 37 | httpthread.start() 38 | logging.info("Local HTTP is " + alexa_params.BASE_URL) 39 | alexa_control.start() 40 | 41 | def close(): 42 | localHTTP.shutdown() 43 | httpthread.join() 44 | zeroconf.unregisterService(info) 45 | zeroconf.close() 46 | alexa_control.close() 47 | 48 | -------------------------------------------------------------------------------- /alexa_audio_device_pulse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import math 4 | import struct 5 | import ctypes 6 | import logging 7 | 8 | class _struct_pa_sample_spec(ctypes.Structure): 9 | _fields_ = [('format', ctypes.c_int), 10 | ('rate', ctypes.c_uint32), 11 | ('channels', ctypes.c_uint8)] 12 | _PA_STREAM_PLAYBACK = 1 13 | _PA_STREAM_RECORD = 2 14 | _PA_SAMPLE_S16LE = 3 15 | 16 | def init(): 17 | global pa, in_stream, out_stream, error 18 | error = ctypes.c_int(0) 19 | pa = ctypes.cdll.LoadLibrary('libpulse-simple.so.0') 20 | pa.strerror.restype = ctypes.c_char_p 21 | ss = _struct_pa_sample_spec(_PA_SAMPLE_S16LE, 16000, 1) 22 | 23 | out_stream = ctypes.c_void_p(pa.pa_simple_new(None, 24 | 'Alexa'.encode('ascii'), _PA_STREAM_PLAYBACK, None, 25 | 'Alexa voice'.encode('ascii'), ctypes.byref(ss), 26 | None, None, ctypes.byref(error))) 27 | if not out_stream: 28 | raise Exception('Could not create pulse audio output stream: ' 29 | + str(pa.strerror(error), 'ascii')) 30 | 31 | in_stream = ctypes.c_void_p(pa.pa_simple_new(None, 32 | 'Alexa'.encode('ascii'), _PA_STREAM_RECORD, None, 33 | 'Alexa mic'.encode('ascii'), ctypes.byref(ss), 34 | None, None, ctypes.byref(error))) 35 | if not in_stream: 36 | raise Exception('Could not create pulse audio input stream: ' 37 | + str(pa.strerror(error), 'ascii')) 38 | logging.info('PulseAudio is initialized.') 39 | 40 | def deinit(): 41 | pa.pa_simple_free(in_stream) 42 | pa.pa_simple_free(out_stream) 43 | 44 | class AlexaAudioDevice: 45 | def __init__(self): 46 | pa.pa_simple_flush(in_stream) 47 | pa.pa_simple_flush(out_stream) 48 | 49 | def close(self): 50 | pa.pa_simple_flush(in_stream) 51 | pa.pa_simple_flush(out_stream) 52 | 53 | def write(self, b): 54 | return pa.pa_simple_write(out_stream, b, len(b), ctypes.byref(error)) 55 | 56 | def flush(self): 57 | pa.pa_simple_flush(out_stream) 58 | 59 | def read(self, n): 60 | data = ctypes.create_string_buffer(n) 61 | pa.pa_simple_read(in_stream, data, n, ctypes.byref(error)) 62 | return data.raw 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa Device 2 | This project allows to launch Alexa on any Linux devices, even on embedded devices like Raspberry Pi (with usb headset). 3 | 4 | # Running 5 | * Follow this manual to create your own device and security profile - https://github.com/alexa/avs-device-sdk/wiki/Create-Security-Profile 6 | Add `http://alexa.local:3000/authresponse` to `Allowed Return URLs` and `http://alexa.local:3000` to `Allowed Origins` 7 | _Note this step can be skipped if you already have device profile credentials._ 8 | * Install dependencies 9 | ```bash 10 | sudo apt install python3-pip git ffmpeg swig libportaudio2 portaudio19-dev libpulse-dev 11 | sudo pip3 install requests 'git+https://github.com/moaxey/python-zeroconf' pocketsphinx pyaudio 12 | ``` 13 | _Note 'libpulse-dev' should be installed only for PulseAudio based devices. 'pyaduio', 'libportaudio2' and 'portaudio19-dev' should be installed on other devices, for example alsa capable._ 14 | * Make sure your system has PulseAudio support. 15 | * Run 16 | ```bash 17 | python3 alexa.py 18 | ``` 19 | * Open http://alexa.local:3000 in web browser on local device or device in the same network. 20 | _Note app provides mDNS advertisement of local domain alexa.local. This is very helpful for using with monitorless devices._ 21 | * Fill device credentials which was created in step 1, click 'log in'. 22 | _Note Voice detection threshold is float value for adjusting voice detection. The less value, the easier to trigger. You may need to adjust it for your mic and voice._ 23 | * Fill your amazon credentials. 24 | * Now you can speak with Alexa. App uses voice activation. Say 'Alexa' and phrase that you want to say to her. App makes beep in speakers when it hear 'Alexa' keyword and start recording. 25 | 26 | # Snap package ( https://www.ubuntu.com/desktop/snappy ) 27 | App can be built as snap package. 28 | Install `pulseaudio` snap before installing this: 29 | ```bash 30 | sudo snap install --devmode pulseaudio 31 | ``` 32 | Now build and install: 33 | ```bash 34 | cd snap 35 | snacraft 36 | sudo snap install --devmode alexa_1.0_amd64.snap 37 | ``` 38 | _Note if you modified snap to used for alsa based devices, then additionally call this line `sudo snap connect alexa:alsa :alsa`._ 39 | 40 | # Similar open source software 41 | Python Alexa Voice Service - https://github.com/nicholasjconn/python-alexa-voice-service 42 | 43 | -------------------------------------------------------------------------------- /alexa_device.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | import threading 5 | import json 6 | import time 7 | import logging 8 | 9 | import alexa_audio 10 | 11 | class AlexaDevice: 12 | def __init__(self, alexa_config): 13 | self.alexa_audio_instance = alexa_audio.AlexaAudio(alexa_config['threshold'], self.send_audio) 14 | self.refresh_token = alexa_config['refresh_token'] 15 | self.client_id = alexa_config['Client_ID'] 16 | self.client_secret = alexa_config['Client_Secret'] 17 | self.access_token = None 18 | self.access_token_time = 0 19 | 20 | def get_access_token(self): 21 | current_time = time.mktime(time.gmtime()) 22 | if self.access_token is None or current_time - self.access_token_time > 3500: 23 | body = { 24 | 'client_id': self.client_id, 25 | 'client_secret': self.client_secret, 26 | 'refresh_token': self.refresh_token, 27 | 'grant_type': 'refresh_token' 28 | } 29 | r = requests.post('https://api.amazon.com/auth/o2/token', data=body) 30 | resp = json.loads(r.text) 31 | self.access_token = resp['access_token'] 32 | self.access_token_time = current_time 33 | return self.access_token 34 | 35 | def check_response(self, content): 36 | jsontypepos = content.find(b'application/json') 37 | if jsontypepos < 0: 38 | return False 39 | jsonpos = content.find(b'\r\n\r\n', jsontypepos) + 4 40 | if jsonpos < 4: 41 | return False 42 | jsonposend = content.find(b'\r\n--', jsonpos) 43 | if jsonposend < 0: 44 | return False 45 | jsontext = content[jsonpos:jsonposend].decode('utf8') 46 | response_metadata = json.loads(jsontext) 47 | if 'messageBody' not in response_metadata: 48 | return False 49 | if 'directives' not in response_metadata['messageBody']: 50 | return False 51 | 52 | for v in response_metadata['messageBody']['directives']: 53 | if 'name' in v: 54 | if v['name'] == 'listen': 55 | return True 56 | return False 57 | 58 | def send_audio(self): 59 | self.get_audio_and_send(); 60 | 61 | def get_audio_and_send(self, timeout = None): 62 | raw_audio = self.alexa_audio_instance.get_audio(timeout) 63 | if raw_audio is None: 64 | return 65 | headers = {'Authorization': 'Bearer ' + self.get_access_token()} 66 | metadata = { 67 | 'messageHeader': {}, 68 | 'messageBody': { 69 | 'profile': 'alexa-close-talk', 70 | 'locale': 'en-us', 71 | 'format': 'audio/L16; rate=16000; channels=1' 72 | } 73 | } 74 | files = [ 75 | ('metadata', (None, json.dumps(metadata).encode('utf8'), 'application/json; charset=UTF-8')), 76 | ('audio', (None, raw_audio, 'audio/L16; rate=16000; channels=1')) 77 | ] 78 | url = 'https://access-alexa-na.amazon.com/v1/avs/speechrecognizer/recognize' 79 | try: 80 | r = requests.post(url, headers=headers, files=files) 81 | except requests.exceptions.ConnectionError as e: 82 | logging.info(type(e).__name__) 83 | time.sleep(0.1) 84 | self.alexa_audio_instance.beep_failed() 85 | return 86 | if r.status_code != requests.codes.ok: 87 | logging.info("Audio response faile with " + str(r.status_code) + " code: " + r.text) 88 | self.alexa_audio_instance.beep_failed() 89 | return 90 | content = r.content 91 | mpegpos = content.find(b'audio/mpeg') 92 | if mpegpos < 0: 93 | logging.info("No audio found in response: " + r.text) 94 | self.alexa_audio_instance.beep_failed() 95 | return 96 | rawmpegpos = content.find(b'\r\n\r\n', mpegpos); 97 | if rawmpegpos < 0: 98 | logging.info("No raw audio data found: " + r.text) 99 | self.alexa_audio_instance.beep_failed() 100 | return 101 | data = content[rawmpegpos + 4:] 102 | logging.info("Alexa got response") 103 | self.alexa_audio_instance.play_mp3(data) 104 | if self.check_response(content): 105 | time.sleep(0.5) 106 | self.get_audio_and_send(5) 107 | 108 | def close(self): 109 | self.alexa_audio_instance.close() 110 | 111 | -------------------------------------------------------------------------------- /alexa_audio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import threading 4 | import math 5 | import struct 6 | import time 7 | import alexa_audio_device 8 | import logging 9 | from subprocess import Popen, PIPE, STDOUT 10 | from pocketsphinx import * 11 | 12 | DETECT_HYSTERESIS = 1.2 # level should fall lower that background noise 13 | DETECT_MIN_LENGTH_S = 2.5 # minimal length of record 14 | DETECT_MAX_LENGTH_S = 10 # minimal amount of buffers to activate 15 | DETECT_BUFFERS_FOR_INIT = 10 # number of buffers for initialising 16 | 17 | class AlexaAudio: 18 | def __init__(self, threshold, callback): 19 | self.ad = alexa_audio_device.AlexaAudioDevice() 20 | self.callback = callback 21 | self.beep_finished_buf = self._beep(150, 1000) 22 | self.beep_short_buf = self._beep(150, 3000) 23 | self.beep_failed_buf = self._beep(600, 400) 24 | self.is_run = True 25 | self.average = 100.0 26 | self.init_counter = 0 27 | self.skip = 0 28 | # init pocketsphinx 29 | config = Decoder.default_config() 30 | config.set_string('-hmm', os.path.join(get_model_path(), 'en-us')) 31 | config.set_string('-dict', os.path.join(get_model_path(), 'cmudict-en-us.dict')) 32 | config.set_string('-logfn', '/dev/null') 33 | config.set_string('-keyphrase', 'alexa') 34 | logging.info("Voice threshold is " + str(threshold)) 35 | config.set_float('-kws_threshold', threshold) 36 | self.decoder = Decoder(config) 37 | self.decoder.start_utt() 38 | self.capture_in_progress = False 39 | self.buffer = None 40 | self.notify = True 41 | self.pt = threading.Thread(target=self.processAudio) 42 | self.pt.start() 43 | 44 | def _beep(self, length_ms = 150, frequency = 1000.0, framerate = 16000, amplitude = 0.2): 45 | period = int(framerate / frequency) 46 | snd = bytes() 47 | for i in range(0, int(framerate * length_ms / 1000)): 48 | val = 32767.0 * amplitude * math.sin(2.0 * math.pi * float(i % period) / period) 49 | snd += struct.pack(' 0: 79 | self.skip -= len(buf) 80 | continue 81 | level = 0 82 | for i in range(0, len(buf), 2): 83 | val = struct.unpack_from(' self.detect_buffer_max: 90 | self.detect_buffer_max = level 91 | duration = len(self.detect_buffer)/16000/2 92 | if duration >= DETECT_MAX_LENGTH_S or ( 93 | duration >= DETECT_MIN_LENGTH_S and 94 | level < self.average * DETECT_HYSTERESIS): 95 | self.capture_in_progress = False 96 | if self.detect_buffer_max > self.average * DETECT_HYSTERESIS: 97 | logging.info("Finished " + str(duration) + "s") 98 | self.buffer = self.detect_buffer 99 | if self.notify: 100 | threading.Thread(target=self.callback).start() 101 | self.skip += 16000 102 | #self.play(self.detect_buffer) 103 | else: 104 | logging.info("Cancel " + str(duration) + "s due to the low level ") 105 | #self.beep_failed() 106 | else: 107 | self.decoder.process_raw(buf, False, False) 108 | if self.decoder.hyp() != None and self.init_counter > DETECT_BUFFERS_FOR_INIT: 109 | self.start_capture() 110 | self.detect_buffer += buf 111 | logging.info("Found Alexa keyword") 112 | self.decoder.end_utt() 113 | self.decoder.start_utt() 114 | else: 115 | if self.init_counter <= DETECT_BUFFERS_FOR_INIT: 116 | if self.init_counter == DETECT_BUFFERS_FOR_INIT: 117 | logging.info("Alexa is initialized and started.") 118 | self.init_counter += 1 119 | self.average = self.average * 0.75 + level * 0.25 120 | logging.info("Audio Processing finished.") 121 | 122 | def close(self): 123 | self.is_run = False 124 | self.pt.join() 125 | self.ad.close() 126 | 127 | def get_audio(self, timeout = None): 128 | if timeout is not None: 129 | self.start_capture(False) 130 | for i in range(int(timeout)): 131 | if(self.buffer is not None): 132 | break 133 | time.sleep(1) 134 | if self.buffer is None: 135 | if self.detect_buffer_max > self.average * DETECT_HYSTERESIS: 136 | res = self.detect_buffer 137 | self.capture_in_progress = False 138 | logging.info('Timeout exceed, phrase might not be completed') 139 | self.beep_finished() 140 | return res 141 | else: 142 | logging.info('Timeout exceed, but nothing was detected') 143 | self.beep_failed() 144 | return None 145 | res = self.buffer 146 | self.buffer = None 147 | if res is not None: 148 | self.beep_finished() 149 | return res 150 | 151 | def play(self, audio): 152 | self.skip += len(audio) 153 | self.ad.write(audio) 154 | 155 | def play_mp3(self, raw_audio): 156 | p = Popen(['ffmpeg', '-i', '-', '-ac', '1', '-acodec', 157 | 'pcm_s16le', '-ar', '16000', '-f', 's16le', '-'], 158 | stdout=PIPE, stdin=PIPE, stderr=PIPE) 159 | pcm = p.communicate(input=raw_audio)[0] 160 | self.play(pcm) 161 | 162 | -------------------------------------------------------------------------------- /alexa_http_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import alexa_params 4 | import alexa_control 5 | import json 6 | import requests 7 | from http.server import BaseHTTPRequestHandler 8 | from cgi import parse_header 9 | from urllib.parse import parse_qs, urlencode, urlparse 10 | 11 | threshold = alexa_params.DEFAULT_VOICE_THRESHOLD 12 | productId = alexa_params.DEFAULT_PRODUCT_ID 13 | clientId = alexa_params.DEFAULT_CLIEND_ID 14 | deviceSerial = alexa_params.DEFAULT_DEVICE_SERIAL 15 | clientSecret = alexa_params.DEFAULT_CLIENT_SECRET 16 | 17 | def load_config(): 18 | global clientId, clientSecret, threshold, deviceSerial, productId 19 | try: 20 | with open(alexa_params.ALEXA_CREDENTIALS_FILE, 'r') as infile: 21 | config = json.load(infile) 22 | clientId = config['Client_ID'] 23 | clientSecret = config['Client_Secret'] 24 | threshold = config['threshold'] 25 | deviceSerial = config['deviceSerial'] 26 | productId = config['productId'] 27 | return config 28 | except IOError: 29 | pass 30 | return None 31 | 32 | class AlexaConfig(BaseHTTPRequestHandler): 33 | def do_POST(self): 34 | global threshold 35 | global productId 36 | global clientId 37 | global deviceSerial 38 | global clientSecret 39 | ctype, pdict = parse_header(self.headers['content-type']) 40 | if ctype == 'multipart/form-data': 41 | postvars = parse_multipart(self.rfile, pdict) 42 | elif ctype == 'application/x-www-form-urlencoded': 43 | length = int(self.headers['content-length']) 44 | postvars = parse_qs(self.rfile.read(length), keep_blank_values=1) 45 | else: 46 | postvars = {} 47 | threshold = float(b''.join(postvars.get(bytes("threshold", "utf-8"))).decode("utf-8")) 48 | productId = b''.join(postvars.get(bytes("productid", "utf-8"))).decode("utf-8") 49 | clientId = b''.join(postvars.get(bytes("clientid", "utf-8"))).decode("utf-8") 50 | deviceSerial = b''.join(postvars.get(bytes("serial", "utf-8"))).decode("utf-8") 51 | clientSecret = b''.join(postvars.get(bytes("secret", "utf-8"))).decode("utf-8") 52 | self.send_response(200) 53 | self.send_header("Content-type", "text/html") 54 | self.end_headers() 55 | url = "https://www.amazon.com/ap/oa?" + urlencode({ 56 | "client_id": clientId, 57 | "scope": "alexa:all", 58 | "scope_data": json.dumps({ 59 | "alexa:all": { 60 | "productID": productId, 61 | "productInstanceAttributes": { 62 | "deviceSerialNumber": deviceSerial 63 | } 64 | } 65 | }).replace(" ", ""), 66 | "response_type": "code", 67 | "redirect_uri": alexa_params.BASE_URL + "authresponse" 68 | }) 69 | self.wfile.write(bytes("Alexa", "utf-8")) 70 | self.wfile.write(bytes("", "utf-8")) 71 | self.wfile.write(bytes("

Redirecting to amazon login service...

", "utf-8")) 72 | self.wfile.write(bytes("", "utf-8")) 73 | 74 | def do_GET(self): 75 | self.send_response(200) 76 | self.send_header("Content-type", "text/html") 77 | self.end_headers() 78 | if self.path.startswith("/authresponse"): 79 | parsed = urlparse(self.path) 80 | code = ''.join(parse_qs(parsed.query).get("code")) 81 | data = { 82 | "grant_type": "authorization_code", 83 | "code": code, 84 | "client_id": clientId, 85 | "client_secret": clientSecret, 86 | "redirect_uri": alexa_params.BASE_URL + "authresponse" 87 | } 88 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 89 | response = requests.post("https://api.amazon.com/auth/o2/token", data=urlencode(data), headers=headers) 90 | if response.status_code == 200: 91 | refresh_token = json.loads(response.text)["refresh_token"] 92 | access_token = json.loads(response.text)["access_token"] 93 | print("---------Access Token----------------") 94 | print(access_token) 95 | print("-------------------------------------") 96 | with open(alexa_params.ALEXA_CREDENTIALS_FILE, 'w') as outfile: 97 | data = { 98 | "threshold": threshold, 99 | "refresh_token": refresh_token, 100 | "Client_ID": clientId, 101 | "Client_Secret": clientSecret, 102 | "deviceSerial": deviceSerial, 103 | "productId": productId 104 | } 105 | json.dump(data, outfile) 106 | alexa_control.start() 107 | self.authorizedAnswer() 108 | else: 109 | self.wfile.write(bytes("Alexa", "utf-8")) 110 | self.wfile.write(bytes("

Authorization error. Click here to try again

", "utf-8")) 111 | self.wfile.write(bytes("", "utf-8")) 112 | elif self.path == "/logout": 113 | try: 114 | os.remove(alexa_params.ALEXA_CREDENTIALS_FILE) 115 | except FileNotFoundError: 116 | pass 117 | alexa_control.close() 118 | self.wfile.write(bytes("Alexa", "utf-8")) 119 | self.wfile.write(bytes("", "utf-8")) 120 | self.wfile.write(bytes("

Done. Reloading...

", "utf-8")) 121 | self.wfile.write(bytes("", "utf-8")) 122 | elif self.path == "/restart": 123 | alexa_control.close() 124 | alexa_control.start() 125 | self.authorizedAnswer(); 126 | elif os.path.isfile(alexa_params.ALEXA_CREDENTIALS_FILE): 127 | self.authorizedAnswer(); 128 | else: 129 | self.wfile.write(bytes(""" 130 | Alexa 131 | 132 |
133 | Voice detection threshold(float value):
134 |

135 | Product ID(Device Type ID):
136 |

137 | Client ID:
138 |

139 | Device Serial Number:
140 |

141 | Client Secret:
142 |

143 | 144 |
145 | """, "utf-8")) 146 | 147 | def authorizedAnswer(self): 148 | self.wfile.write(bytes("Alexa", "utf-8")) 149 | self.wfile.write(bytes(" 1) window.location.href='" + alexa_params.BASE_URL + "'\">

Alexa is configured and started.

", "utf-8")) 150 | self.wfile.write(bytes("

Click here to log out.

", "utf-8")) 151 | self.wfile.write(bytes("

Click here to restart Alexa.

", "utf-8")) 152 | self.wfile.write(bytes("", "utf-8")) 153 | 154 | def log_message(self, format, *args): 155 | return 156 | 157 | --------------------------------------------------------------------------------