* 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:
Homer S. 2021-06-29 00:27:27 +02:00
parent e4e5185735
commit a086cd15eb
7 changed files with 255 additions and 152 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
*~
docker-compose.yml

3
.gitignore vendored
View File

@ -141,3 +141,6 @@ cython_debug/
# matrix-commander # matrix-commander
/store /store
credentials.json credentials.json
# emacs
*~

35
docker/Dockerfile Normal file
View File

@ -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

16
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,16 @@
version: "3.7"
services:
app:
container_name: "fritzab2matrix"
build:
context: ../.
dockerfile: ./docker/Dockerfile
working_dir: /app
volumes:
- ./:/app

View File

@ -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)

View File

@ -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))

View File

@ -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)