* Added docker folder to repo
* Ubuntu based docker file can be build from the repo's root by ``` docker-compose -f docker/docker-compose.yml build ``` * and started by ``` docker-compose -f docker/docker-compose.yml build ``` * Restructured code by packaging the CallMonitoring and SpeexConverting * main() runs on startup and then watches for disconnected calls as trigger for running again. * Commented main file
This commit is contained in:
parent
e4e5185735
commit
a086cd15eb
|
@ -0,0 +1,2 @@
|
||||||
|
*~
|
||||||
|
docker-compose.yml
|
|
@ -141,3 +141,6 @@ cython_debug/
|
||||||
# matrix-commander
|
# matrix-commander
|
||||||
/store
|
/store
|
||||||
credentials.json
|
credentials.json
|
||||||
|
|
||||||
|
# emacs
|
||||||
|
*~
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM ubuntu:latest
|
||||||
|
|
||||||
|
ENV TZ=Europe/Berlin
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
VOLUME /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN ls -la
|
||||||
|
RUN ["cat", "/app/requirements.txt"]
|
||||||
|
|
||||||
|
RUN /bin/bash -c 'apt update && apt install -y libolm-dev python3-pip ffmpeg;'
|
||||||
|
|
||||||
|
|
||||||
|
RUN pip install update pip && pip install -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
CMD python3 fritzab2matrix.py && tail -f /dev/null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: "fritzab2matrix"
|
||||||
|
build:
|
||||||
|
context: ../.
|
||||||
|
dockerfile: ./docker/Dockerfile
|
||||||
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
|
||||||
|
|
||||||
from fritzconnection import FritzConnection
|
from fritzconnection import FritzConnection
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
|
from libs.monitoring import endedCall
|
||||||
|
from libs.message import conversion as conv
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import xmltodict
|
import xmltodict
|
||||||
import sys, os
|
import sys, os
|
||||||
import smbclient
|
import smbclient
|
||||||
import ctypes
|
|
||||||
import wave
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,179 +16,106 @@ load_dotenv()
|
||||||
env_user = os.environ.get('FRITZ_USERNAME')
|
env_user = os.environ.get('FRITZ_USERNAME')
|
||||||
env_pass = os.environ.get('FRITZ_PASSWORD')
|
env_pass = os.environ.get('FRITZ_PASSWORD')
|
||||||
env_ip = os.environ.get('FRITZ_IP')
|
env_ip = os.environ.get('FRITZ_IP')
|
||||||
|
env_tmp = os.environ.get('TEMP_DIR')
|
||||||
|
if env_tmp is None:
|
||||||
|
env_tmp = "/tmp"
|
||||||
|
|
||||||
### CHECK AND GET MESSAGES FROM FRITZBOX ###
|
def main():
|
||||||
############################################
|
|
||||||
|
|
||||||
## Connect to the FritzBox in the LAN
|
### CHECK AND GET MESSAGES FROM FRITZBOX ###
|
||||||
# We don't use tls because the self-signed cert of the box leads to a malfunction in urllib later on.
|
############################################
|
||||||
fc = FritzConnection(address=env_ip, user=env_user, password=env_pass, use_tls=False)
|
|
||||||
|
## Connect to the FritzBox in the LAN
|
||||||
|
# We don't use tls because the self-signed cert of the box leads to a malfunction in urllib later on.
|
||||||
|
fc = FritzConnection(address=env_ip, user=env_user, password=env_pass, use_tls=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Get info about messages from the main answering machine
|
## Get info about messages from the main answering machine
|
||||||
message_list = fc.call_action("X_AVM-DE_TAM1", "GetMessageList", NewIndex=0)
|
message_list = fc.call_action("X_AVM-DE_TAM1", "GetMessageList", NewIndex=0)
|
||||||
message_list_url = message_list['NewURL']
|
message_list_url = message_list['NewURL']
|
||||||
|
|
||||||
|
|
||||||
### Convert fb speex format to wav ###
|
# Build the url to download the message via smb
|
||||||
######################################
|
def build_download_url(mid, tam=0):
|
||||||
# c&p that from https://git.savannah.nongnu.org/cgit/fbvbconv-py.git/tree/fbvbconv.py
|
url = r"//" + env_ip + r"/fritz.nas/FRITZ/voicebox/rec/rec." + str(tam) + r"." + str(mid).zfill(3)
|
||||||
|
return url
|
||||||
|
|
||||||
speexlib = ctypes.cdll.LoadLibrary("libspeex.so.1")
|
def download_speex_file(smb_url):
|
||||||
SPEEX_SET_SAMPLING_RATE = 24
|
smbclient.register_session(server=env_ip, username=env_user, password=env_pass, auth_protocol="ntlm")
|
||||||
SPEEX_GET_FRAME_SIZE = 3
|
fd = smbclient.open_file(smb_url, mode="rb")
|
||||||
|
return fd
|
||||||
class SpeexMode(ctypes.c_void_p):
|
|
||||||
pass
|
|
||||||
class Speex(ctypes.c_void_p):
|
|
||||||
pass
|
|
||||||
class SpeexBits(ctypes.Structure):
|
|
||||||
_fields_ = [
|
|
||||||
('chars', ctypes.c_char_p),
|
|
||||||
('nbBits', ctypes.c_int),
|
|
||||||
('charPtr', ctypes.c_int),
|
|
||||||
('bitPtr', ctypes.c_int),
|
|
||||||
('owner', ctypes.c_int),
|
|
||||||
('overflow', ctypes.c_int),
|
|
||||||
('but_size', ctypes.c_int),
|
|
||||||
('reserved1', ctypes.c_int),
|
|
||||||
('reserved2', ctypes.c_void_p)
|
|
||||||
]
|
|
||||||
|
|
||||||
speex_lib_get_mode = speexlib.speex_lib_get_mode
|
|
||||||
speex_lib_get_mode.restype = SpeexMode
|
|
||||||
speex_decoder_init = speexlib.speex_decoder_init
|
|
||||||
speex_decoder_init.restype = Speex
|
|
||||||
speex_decoder_ctl = speexlib.speex_decoder_ctl
|
|
||||||
speex_bits_init = speexlib.speex_bits_init
|
|
||||||
speex_bits_read_from = speexlib.speex_bits_read_from
|
|
||||||
speex_decode_int = speexlib.speex_decode_int
|
|
||||||
speex_bits_remaining = speexlib.speex_bits_remaining
|
|
||||||
speex_bits_destroy = speexlib.speex_bits_destroy
|
|
||||||
|
|
||||||
|
|
||||||
def speex_convert(inp, outp):
|
def get_message_list(url):
|
||||||
# rec = open(inp, 'rb').read()
|
""" Get and and convert the xml formatted list of messages into a dictionary. """
|
||||||
rec = inp.read()
|
with urllib.request.urlopen(url) as f:
|
||||||
wav = wave.open(outp, 'wb')
|
doc = f.read()
|
||||||
wav.setnchannels(1)
|
# Convert the xml formatted message list to dict
|
||||||
wav.setsampwidth(2)
|
messages = xmltodict.parse(doc)
|
||||||
wav.setframerate(8000)
|
return messages
|
||||||
|
|
||||||
mode = speex_lib_get_mode(0)
|
|
||||||
speex = speex_decoder_init(mode)
|
|
||||||
speex_decoder_ctl(speex, SPEEX_SET_SAMPLING_RATE, ctypes.byref(ctypes.c_int(8000)))
|
|
||||||
bits = SpeexBits()
|
|
||||||
speex_bits_init(ctypes.byref(bits))
|
|
||||||
frame_size = ctypes.c_int()
|
|
||||||
speex_decoder_ctl(speex, SPEEX_GET_FRAME_SIZE, ctypes.byref(frame_size))
|
|
||||||
|
|
||||||
output = ctypes.create_string_buffer(2000)
|
|
||||||
offs = 0
|
|
||||||
while offs < len(rec):
|
|
||||||
nbytes = rec[offs]
|
|
||||||
offs += 1
|
|
||||||
if nbytes != 0x26:
|
|
||||||
continue
|
|
||||||
buf = ctypes.create_string_buffer(rec[offs:offs + nbytes])
|
|
||||||
offs += nbytes
|
|
||||||
speex_bits_read_from(ctypes.byref(bits), buf, ctypes.c_int(nbytes))
|
|
||||||
# this loop looks strange, but its like in roger router and seems to work
|
|
||||||
for i in range(2):
|
|
||||||
rc = speex_decode_int(speex, ctypes.byref(bits), output)
|
|
||||||
if rc == -1:
|
|
||||||
break
|
|
||||||
elif rc == -2:
|
|
||||||
print("Decoding error: corrupted stream?");
|
|
||||||
break
|
|
||||||
if speex_bits_remaining(ctypes.byref(bits)) < 0:
|
|
||||||
print("Decoding overflow: corrupted stream?");
|
|
||||||
break
|
|
||||||
wav.writeframes(output[0:2 * frame_size.value])
|
|
||||||
|
|
||||||
wav.close()
|
|
||||||
speex_bits_destroy(ctypes.byref(bits))
|
|
||||||
|
|
||||||
|
|
||||||
|
for a in get_message_list(message_list_url)['Root']['Message']:
|
||||||
|
|
||||||
# Build the url to download the message via smb
|
# format the information regarding the message
|
||||||
# smb://192.168.1.1/fritz.nas/FRITZ/voicebox/rec
|
msg_info = a['Date'] + " - " + a['Number']
|
||||||
|
if len(a['Name']) > 1:
|
||||||
|
msg_info += " (" + a['Name'] + ") "
|
||||||
|
|
||||||
def build_download_url(mid, tam=0):
|
# format the string for sound file's meta information
|
||||||
url = r"//" + env_ip + r"/fritz.nas/FRITZ/voicebox/rec/rec." + str(tam) + r"." + str(mid).zfill(3)
|
msg_tags = {'title': msg_info, 'artist': 'Answerting Machine' ,'album': "TAM" + a['Tam'], 'comment': 'Message of a telephone answering machine'}
|
||||||
return url
|
|
||||||
|
|
||||||
def download_speex_file(smb_url):
|
# Select only new messages
|
||||||
smbclient.register_session(server=env_ip, username=env_user, password=env_pass, auth_protocol="ntlm")
|
message_new = bool(int(a['New']))
|
||||||
fd = smbclient.open_file(smb_url, mode="rb")
|
|
||||||
return fd
|
|
||||||
|
|
||||||
|
if message_new == True:
|
||||||
|
|
||||||
with urllib.request.urlopen(message_list_url) as f:
|
# Download and convert the speex files to wav
|
||||||
doc = f.read()
|
smb_url = build_download_url(a['Index'])
|
||||||
# Convert the xml formatted message list to dict
|
speex_fd = download_speex_file(smb_url)
|
||||||
messages = xmltodict.parse(doc)
|
conv.speex_convert(speex_fd, os.path.join(env_tmp,"message.wav"))
|
||||||
|
# Convert wav to ogg
|
||||||
|
msg = AudioSegment.from_wav(os.path.join(env_tmp,"message.wav"))
|
||||||
|
|
||||||
|
# Only if message is longer than 5 seconds ...
|
||||||
|
if msg.duration_seconds > 5.0:
|
||||||
|
# ... export to ogg ...
|
||||||
|
msg.export(os.path.join(env_tmp,"message.ogg"), format="ogg", tags=msg_tags)
|
||||||
|
|
||||||
for a in messages['Root']['Message']:
|
# ... and send message and file to Matrix Room
|
||||||
|
command = "python3 matrix-commander.py -a " + os.path.join(env_tmp,"message.ogg") + " -m '{}'".format(msg_info)
|
||||||
|
# os.system(command)
|
||||||
|
|
||||||
msg_info = a['Date'] + " - " + a['Number']
|
else:
|
||||||
if len(a['Name']) > 1:
|
# Mark MessageInfo as too short for the log
|
||||||
msg_info += " (" + a['Name'] + ") "
|
msg_info += " < 6 sec (not posted)"
|
||||||
msg_tags = {'title': msg_info, 'artist': 'Answerting Machine' ,'album': "TAM" + a['Tam'], 'comment': 'Message of a telephone answering machine'}
|
|
||||||
|
|
||||||
# Select only new messages
|
# Show that message is new
|
||||||
message_new = bool(int(a['New']))
|
print("** " + msg_info)
|
||||||
|
|
||||||
# Select only messages longer than 5 sec
|
# Mark processed messages as 'read'
|
||||||
# No. Unfortunaetly the MessageList excludingly puts 0:01 into Duration tag.
|
fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=1)
|
||||||
|
|
||||||
if message_new == True:
|
|
||||||
|
|
||||||
smb_url = build_download_url(a['Index'])
|
|
||||||
speex_fd = download_speex_file(smb_url)
|
|
||||||
speex_convert(speex_fd, "/tmp/message.wav")
|
|
||||||
# Convert wav to ogg
|
|
||||||
msg = AudioSegment.from_wav("/tmp/message.wav")
|
|
||||||
|
|
||||||
# Only convert and upload if message is longer than 5 seconds.
|
|
||||||
if msg.duration_seconds > 5.0:
|
|
||||||
msg.export("/tmp/message.ogg", format="ogg", tags=msg_tags)
|
|
||||||
|
|
||||||
### POST MESSAGES TO MATRIX PRIVATE CHAT ###
|
|
||||||
############################################
|
|
||||||
|
|
||||||
# Formatting message
|
|
||||||
|
|
||||||
# Send message and file to Matrix Room
|
|
||||||
command = "python3 matrix-commander.py -a /tmp/message.ogg -m '{}'".format(msg_info)
|
|
||||||
os.system(command)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Mark MessageInfo as too short
|
# Show that message is already read
|
||||||
msg_info += " < 6 sec (not posted)"
|
print("__ " + msg_info)
|
||||||
print("** " + msg_info)
|
|
||||||
|
|
||||||
# Mark processed messages as 'read'
|
# ## For testing purposes only
|
||||||
fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=1)
|
# if a['Date'].endswith('20:53'):
|
||||||
|
# fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=0)
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("__ " + msg_info)
|
|
||||||
|
|
||||||
## For testing purposes only
|
|
||||||
if a['Number'].endswith('7714'):
|
|
||||||
fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=0)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
### Monitor the FritzBox and trigger the main script whenever a call disconnects ###
|
||||||
|
###################################################################################
|
||||||
|
endedCall(main, env_ip)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
|
||||||
|
import wave, ctypes
|
||||||
|
|
||||||
|
|
||||||
|
### Convert fb speex format to wav ###
|
||||||
|
######################################
|
||||||
|
"""
|
||||||
|
This piece of code is just copied & pasted from https://git.savannah.nongnu.org/cgit/fbvbconv-py.git/tree/fbvbconv.py
|
||||||
|
which has kindly managed to convert the messages of the answering machine of fritzboxes from a specially configured Speex format to a wave file. All appreciation to the author(s).
|
||||||
|
"""
|
||||||
|
|
||||||
|
speexlib = ctypes.cdll.LoadLibrary("libspeex.so.1")
|
||||||
|
SPEEX_SET_SAMPLING_RATE = 24
|
||||||
|
SPEEX_GET_FRAME_SIZE = 3
|
||||||
|
|
||||||
|
class SpeexMode(ctypes.c_void_p):
|
||||||
|
pass
|
||||||
|
class Speex(ctypes.c_void_p):
|
||||||
|
pass
|
||||||
|
class SpeexBits(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('chars', ctypes.c_char_p),
|
||||||
|
('nbBits', ctypes.c_int),
|
||||||
|
('charPtr', ctypes.c_int),
|
||||||
|
('bitPtr', ctypes.c_int),
|
||||||
|
('owner', ctypes.c_int),
|
||||||
|
('overflow', ctypes.c_int),
|
||||||
|
('but_size', ctypes.c_int),
|
||||||
|
('reserved1', ctypes.c_int),
|
||||||
|
('reserved2', ctypes.c_void_p)
|
||||||
|
]
|
||||||
|
|
||||||
|
speex_lib_get_mode = speexlib.speex_lib_get_mode
|
||||||
|
speex_lib_get_mode.restype = SpeexMode
|
||||||
|
speex_decoder_init = speexlib.speex_decoder_init
|
||||||
|
speex_decoder_init.restype = Speex
|
||||||
|
speex_decoder_ctl = speexlib.speex_decoder_ctl
|
||||||
|
speex_bits_init = speexlib.speex_bits_init
|
||||||
|
speex_bits_read_from = speexlib.speex_bits_read_from
|
||||||
|
speex_decode_int = speexlib.speex_decode_int
|
||||||
|
speex_bits_remaining = speexlib.speex_bits_remaining
|
||||||
|
speex_bits_destroy = speexlib.speex_bits_destroy
|
||||||
|
|
||||||
|
|
||||||
|
def speex_convert(inp, outp):
|
||||||
|
# rec = open(inp, 'rb').read()
|
||||||
|
rec = inp.read()
|
||||||
|
wav = wave.open(outp, 'wb')
|
||||||
|
wav.setnchannels(1)
|
||||||
|
wav.setsampwidth(2)
|
||||||
|
wav.setframerate(8000)
|
||||||
|
|
||||||
|
mode = speex_lib_get_mode(0)
|
||||||
|
speex = speex_decoder_init(mode)
|
||||||
|
speex_decoder_ctl(speex, SPEEX_SET_SAMPLING_RATE, ctypes.byref(ctypes.c_int(8000)))
|
||||||
|
bits = SpeexBits()
|
||||||
|
speex_bits_init(ctypes.byref(bits))
|
||||||
|
frame_size = ctypes.c_int()
|
||||||
|
speex_decoder_ctl(speex, SPEEX_GET_FRAME_SIZE, ctypes.byref(frame_size))
|
||||||
|
|
||||||
|
output = ctypes.create_string_buffer(2000)
|
||||||
|
offs = 0
|
||||||
|
while offs < len(rec):
|
||||||
|
nbytes = rec[offs]
|
||||||
|
offs += 1
|
||||||
|
if nbytes != 0x26:
|
||||||
|
continue
|
||||||
|
buf = ctypes.create_string_buffer(rec[offs:offs + nbytes])
|
||||||
|
offs += nbytes
|
||||||
|
speex_bits_read_from(ctypes.byref(bits), buf, ctypes.c_int(nbytes))
|
||||||
|
# this loop looks strange, but its like in roger router and seems to work
|
||||||
|
for i in range(2):
|
||||||
|
rc = speex_decode_int(speex, ctypes.byref(bits), output)
|
||||||
|
if rc == -1:
|
||||||
|
break
|
||||||
|
elif rc == -2:
|
||||||
|
print("Decoding error: corrupted stream?");
|
||||||
|
break
|
||||||
|
if speex_bits_remaining(ctypes.byref(bits)) < 0:
|
||||||
|
print("Decoding overflow: corrupted stream?");
|
||||||
|
break
|
||||||
|
wav.writeframes(output[0:2 * frame_size.value])
|
||||||
|
|
||||||
|
wav.close()
|
||||||
|
speex_bits_destroy(ctypes.byref(bits))
|
|
@ -0,0 +1,36 @@
|
||||||
|
import queue
|
||||||
|
from fritzconnection.core.fritzmonitor import FritzMonitor
|
||||||
|
|
||||||
|
### Monitor the calls of a fritzbox continously ###
|
||||||
|
###################################################
|
||||||
|
|
||||||
|
def watch_disconnect(monitor, event_queue, func, healthcheck_interval=10):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
event = event_queue.get(timeout=healthcheck_interval)
|
||||||
|
except queue.Empty:
|
||||||
|
# check health:
|
||||||
|
if not monitor.is_alive:
|
||||||
|
raise OSError("Error: fritzmonitor connection failed")
|
||||||
|
else:
|
||||||
|
# do event processing here:
|
||||||
|
print(event)
|
||||||
|
if 'DISCONNECT' in event:
|
||||||
|
print("Anruf beendet. Jetzt den AB checken.\n")
|
||||||
|
func()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def endedCall(func, fritz_ip='192.168.1.1'):
|
||||||
|
"""
|
||||||
|
Call this to trigger a given function if a call is disconnected
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# as a context manager FritzMonitor will shut down the monitor thread
|
||||||
|
with FritzMonitor(address=fritz_ip) as monitor:
|
||||||
|
event_queue = monitor.start()
|
||||||
|
watch_disconnect(monitor, event_queue, func)
|
||||||
|
except (OSError, KeyboardInterrupt) as err:
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue