Mac OSx export Adressbook/Adressbuch für AVM Fritzbox

Für uns iPhone- und Mac-Jünger ist die Mac OS X Adressverwaltung wunderbar integriert. Leider fehlte mir bisher eine Möglichkeit zum abgleich des Adressbuchs mit der Fritzbox. Das folgende Python-Programm schrieb ich, um dem abhilfe zu verschaffen. Ich suche noch immer nach einer Möglichkeit, das Programm per automatischen job einzuplanen – Hilfe wäre mir hier sehr willkommen!?

Das Python-Script zum ausführen und
Download: iPyFritz

Der Aufruf erfolgt nach dem entpacken per Doppelklick oder im Terminal mithilfe folgenden Befehls:

Python iPyFritz.py

Dies erzeugt die Datei: myfritz.adressbuch.xml  – welche in der Fritzbox unter “Telefonbuch – Wiederherstellen” eingespielt werden kann. VORSICHT – ALLE ALTEN EINTRÄGE WERDEN GELÖSCHT!!!

Für alle die ein paar Euro ausgeben möchten und können, könnte das hier interessanter sein: fritz.mac Suite. Wobei es in meinen Tests nicht die gleichen Ziele wie mein Adressbuch-exporter hat und die Fax-Anbindung an der 7370 zu abbrüchen führte…

Und hier das Coding / Thx a lot to programmish ); for releasing his snippet under public domain.

Update 11.04.2010
Nur Nickname-Fehler korrigiert

Mazo-Kategorie Update 17.04.2010
Das Script setzt das “Wichtig”-Flag für Personen die einen Nickname gepflegt haben im Adressbuch.

Update 23.04.2010
weitere Alternativen: Fakes Blog

Update 12.05.2010
Suche immer noch nach einer Möglichkeit per launchd (cron alternative) das Adressbuch automatisch zu syncen… Über einen Tipp würde ich mich sehr freuen.

from AddressBook import *
import re
from xml.dom.minidom import *

def addressBookToList():
        """
Copyright (c) 2010, J.Rumpf, www.web-dreamer.de / BSD Licence
All rights reserved.

original found on: http://www.programmish.com/?p=26 released under public domain

Read the current user's AddressBook database, converting each person
in the address book into a Dictionary of values. Some values (addresses,
phone numbers, email, etc) can have multiple values, in which case a
list of all of those values is stored. The result of this method is
a List of Dictionaries, with each person represented by a single record
in the list.

Redistribution and use in source and binary forms, with or without modification
are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, th
is list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
Neither the name of the owner nor the names of its contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

        Read the current user's AddressBook database, converting each person
        in the address book into a Dictionary of values. Some values (addresses,
        phone numbers, email, etc) can have multiple values, in which case a
        list of all of those values is stored. The result of this method is
        a List of Dictionaries, with each person represented by a single recordq
        in the list.
        """

        # get the shared addressbook and the list of
        # people from the book.
        ab = ABAddressBook.sharedAddressBook()
        people = ab.people()
        peopleList = []

        # convert the ABPerson to a hash
        for person in people:
                thisPerson = {}
                props = person.allProperties()
                for prop in props:

                        # skip some properties
                        if prop == "com.apple.ABPersonMeProperty":
                            continue
                        elif prop == "com.apple.ABImageData":
                            continue

                        # How we convert the value depends on the ObjC
                        # class used to represent it
                        val = person.valueForProperty_(prop)
                        if (type(val) == objc.pyobjc_unicode) and val != None:
                            if val.canBeConvertedToEncoding_(NSISOLatin1StringEncoding): #NSUTF8StringEncoding
                                thisPerson[prop.lower()] = val.cStringUsingEncoding_(NSISOLatin1StringEncoding)
                            else:
                 # Unicode String
                                    thisPerson[prop.lower()] = '* '
                    thisPerson[prop.lower()] += val.cStringUsingEncoding_(NSUTF8StringEncoding)
                             #   thisPerson[prop.lower()] = thisPerson[prop.lower()].encode('iso-8859-1', 'xmlcharrefreplace')
                        elif issubclass(val.__class__, NSDate):
                                # NSDate
                                thisPerson[prop.lower()] = val.description()
                        elif type(val) == ABMultiValueCoreDataWrapper:
                                # List -- convert each item in the list
                                # into the proper format
                                thisPerson[prop.lower()] = {} #[]                                  
                                for valIndex in range(0, val.count()):
                                        indexedValue = val.valueAtIndex_(valIndex)
                                        indexedKey = val.labelAtIndex_(valIndex)
                                        if indexedKey == val.primaryIdentifier:
                                            indexedKey += '+'
                                        if type(indexedValue) == objc.pyobjc_unicode:
                                                # Unicode string
                                                thisPerson[prop.lower()].update({indexedKey.cStringUsingEncoding_(4): indexedValue.cStringUsingEncoding_(4)})
                                        elif issubclass(indexedValue.__class__, NSDate):
                                                # Date
                                                thisPerson[prop.lower()].update({prop.lower(): indexedValue.description()})
                                        elif type(indexedValue) == NSCFDictionary:
                                                # NSDictionary -- convert to a Python Dictionary
                                                propDict = {}
                                                for propKey in indexedValue.keys():
                                                        propValue = indexedValue[propKey]
                                                        propDict[propKey.lower()] = propValue
                                                thisPerson[prop.lower()].update(propDict)
                peopleList.append(thisPerson)
        return peopleList

format = re.compile('[ -()-]')
m49 = re.compile('\+49')
mlist = addressBookToList()
#print kABPhoneHomeLabel
#print kABPhoneMobileLabel
#print kABPhoneWorkLabel
#print kABOtherLabel
#print mlist[25]

"""
<phonebooks>
<phonebook>
    <contact>
        <category>0</category>
        <person><realName></realName></person>
        <telephony>
            <number type="home" vanity="" prio="1" quickdial="4"></number>
            <number type="mobile" vanity="" prio="0"></number>
            <number type="work" vanity="" prio="0"></number>
        </telephony>
        <services />
        <setup />
    </contact>
"""

# Create the minidom document
doc = Document()

phonebooks = doc.createElement("phonebooks")
doc.appendChild(phonebooks)

phonebook = doc.createElement("phonebook")
phonebooks.appendChild(phonebook)


for pers in mlist:
    name = pers.get(kABLastNameProperty.lower(),'') + ', ' + pers.get(kABFirstNameProperty.lower(),'')
    if name == ', ':
        name = pers.get(kABOrganizationProperty.lower())
   
    if name == None:
        name = pers.get(kABNicknameProperty.lower())
#       print name
       
    if name == None:
        name = "NoName?"
       
    contact = doc.createElement("contact")
    phonebook.appendChild(contact)
   
    category = doc.createElement("category")
    contact.appendChild(category)
    # person with nik is important >:
    nikname = pers.get(kABNicknameProperty.lower())
    if nikname == None:
        cat = "0"
    else:
        cat = "1"
       
    cat_0 = doc.createTextNode(cat)
    category.appendChild(cat_0)
   
    person = doc.createElement("person")
    contact.appendChild(person)
   
    realname = doc.createElement("realName")
    person.appendChild(realname)
   
    p_inhalt = doc.createTextNode(name)
    realname.appendChild(p_inhalt)
   
    pers_phone = pers.get(kABPhoneProperty.lower())
    if pers_phone != None:
        telephony = doc.createElement("telephony")
        contact.appendChild(telephony)
       
#       quickdial = format.sub('' ,pers_phone.get( "Quickdial", '' ), 0)
        prio = 1
        home = format.sub('' ,pers_phone.get(kABPhoneHomeLabel,''), 0)
        home = m49.sub('0', home)
        if home != '':
            number = None
            number = doc.createElement("number")
            telephony.appendChild(number)      
            number.setAttribute("type", "home")
            number.setAttribute("vanity", "")
            number.setAttribute("prio", '%d' % prio)
            prio += 1
            #number.setAttribute("quickdial", "")
            num_home = doc.createTextNode(home)
            number.appendChild(num_home)


        mobile = format.sub('', pers_phone.get( kABPhoneMobileLabel, '' ), 0)
        mobile = m49.sub('0', mobile )
        if mobile != '':
            number = None
            number = doc.createElement("number")
            telephony.appendChild(number)
            number.setAttribute("type", "mobile")
            number.setAttribute("vanity", "")
            number.setAttribute("prio", '%d' % prio )
            prio += 1
            #number.setAttribute("quickdial", "")
            num_home = doc.createTextNode(mobile)
            number.appendChild(num_home)
       
        work = format.sub('' ,pers_phone.get( kABPhoneWorkLabel, ''), 0)
        work = m49.sub('0', work )
        if work != '':
            number = None
            number = doc.createElement("number")
            telephony.appendChild(number)
            number.setAttribute("type", "work")
            number.setAttribute("vanity", "")
            number.setAttribute("prio", '%d' % prio)
            prio += 1
            #number.setAttribute("quickdial", "")
            num_home = doc.createTextNode(work)
            number.appendChild(num_home)
       
        if ((home == '') and (mobile == '') and ( work == '')):
            other = format.sub('' ,pers_phone.get( kABOtherLabel, '' ), 0)
            other = m49.sub('0', other )
            number = doc.createElement("number")
            telephony.appendChild(number    )
            number.setAttribute("type", "home")
            number.setAttribute("vanity", "")
            number.setAttribute("prio", '%d' % prio)
            prio += 1
            #number.setAttribute("quickdial", "")
            num_home = doc.createTextNode(other)
            number.appendChild(num_home)


f = open('myfritz.adressbuch.xml', 'w')
doc.writexml(f, '', '', '' , 'iso-8859-1')

24 Kommentare zu “Mac OSx export Adressbook/Adressbuch für AVM Fritzbox”

  1. Lars sagt:

    Hi!

    Habe folgendes Problem.

    Benutze die aktuelle Snow Leopard Version und erhalte folgende Fehlermeldung.

    Irgendeine Idee was hier falsch laeuft?

    Vielen Dank im Voraus.

    Gruss,

    Lars

     Python iPyFritz.py
    Traceback (most recent call last):
      File "iPyFritz.py", line 125, in
        p_inhalt = doc.createTextNode(name)
      File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/xml/dom/minidom.py", line 1611, in createTextNode
        raise TypeError, "node contents must be a string"
    TypeError: node contents must be a string
  2. Joe sagt:

    Hi,
    ja, ich denke ich ahne woran es liegt:
    Ich versuche den Nach- und Vornamen zu ermitteln und mit Komma zusammenzusetzen:

    name = pers.get(kABLastNameProperty.lower(),'') + ', ' + pers.get(kABFirstNameProperty.lower(),'')
        if name == ', ':
            name = pers.get(kABOrganizationProperty.lower())

    Wenn das nicht klappt versuche ich es mit dem Organisations-Namen also Firmen-Namen.
    Das heisst Du hast da vermutlich einen Eintrag der keinen Vor- oder Nachnamen enthält. Oder vielleicht hast du auch einen Spitznamen verwendet ohne Vor und Nachnamen – jedenfalls ist die Variable “name” leer und deswegen sagt er das es kein String ist.
    Auf die Entfernung würde ich darum bitten ein paar “print” Befehle abzusetzen um den verqueren Adressbuch-Eintrag zu identifizieren. Im zweifel hilft ein
    if name != None:

    Joe

  3. johannes sagt:

    Ok, das mit Nick hat exakt diesen Fehler produziert – Ich habe das ZIP File aktualisiert – versuch noch mal mit der neuen Version… Viel Spass

  4. mazo sagt:

    Hallo und vielen Dank für das tolle Skript.

    Ich kann leider gar kein Python. Aber äre es noch möglich die “wichtig” Variable zu berücksichtigen? Das ist meine Hauptanwendung für das Fritztelefonbuch, dass Leute die als “wichtig” markiert sind anders klingeln und eben durch die Klingelsperre kommen.

    Z. B. könnte man aus dem Adressbuch das Feld “Friend” (kABFriendLabel) verwenden. Wenn dort nichts ist, dann 0, wenn dort 1 steht, dann 1.

    Die Krönung wäre noch, wenn sich das Skript ohne Interaktion auf die Fritzbox laden könnte. ;-)

    Danke und Gruß,
    Marco

  5. mazo sagt:

    Irgendwie sind die Sachen in spitzen Klammern verloren gegangen.

    ich meinte wenn a “Friend” (kABFriendLabel) leer, dann (category)0(/category), wenn 1, dann (category)1(/category).

    Anstelle runder Klammern bitte spitze denken ;-)

  6. Joe sagt:

    Hi Mazo,
    Danke das dir mein Skript gefällt – ich nehms mal als Anregung auf – und vielleicht habe ich demnächst mal Zeit. :-)
    Joe

  7. johannes sagt:

    So – Wenn ein Nickname gepflegt ist bekommt der Eintrag das “Wichtig”-Flag. Was ich noch ganz nett fände wäre es Schnellwahl-Einträge zu pflegen – das wir jedoch schwierig.

    Zu der automatisierten übertragung an die Fritz.box – grundsätzlich kein Problem – nur muss man dann trotzdem irgendwo das Passwort hinterlegen.

  8. thomas sagt:

    Hi, ich habe das Problem dass polnische Sonderzeichen nicht richtig codiert werden. Statt einem Ł oder ą oder ś erscheint immer ein Å wenn ich die Datei importiere. Kann man was im script an den codiereinstellungen ändern und wenn ja, was? mir würde schon reichen wenn er aus einem Ł ein L und aus einem ą ein a macht. Danke & Gruß
    thomas

  9. Joe sagt:

    Hi Thomas,
    Deutsch ist mit ISO-8859-1 kodiert – so wie es aussieht ist Polnisch mit ISO-8859-2 kodiert (http://alis.isoc.org/codage/iso8859/jeuxiso.de.htm).

    Versuch mal in der letzten Zeile des Scripts oben statt:

    doc.writexml(f, '', '', ''  , 'iso-8859-1')
    ->
    doc.writexml(f, '', '', ''  , 'iso-8859-2')

    Das Problem könnte aber noch an 1-2 anderen Stellen aufschlagen – zB. in der Fritzbox die bei mir im Export das ‘iso-8859-1′ explizit verlangt. MacosX und Python arbeiten beide mit UTF8 insofern könnte es klappen – poste mal das ergebnis…

  10. Joe sagt:

    Achso – wenn du mit Polnisch kodierst fliegen dir natürlich die Deutschen umlaute äüö raus – Ist leider nicht besser zu lösen – schöner wäre es, wenn die Fritzbox mit UTF8 umgehen könnte.

  11. thomas sagt:

    nope, da kommt der gleiche kram bei raus. die schwierigkeit liegt eher im herauskonvertieren der zeichen aus dem adressbuch. welche kodierung müsste ich verwenden damit python die sonderzeichen “glättet” dh aus einem ä ein a und aus einem ą ein a macht?

  12. Joe sagt:

    Hi thomas,
    ich sehe gerade im Coding sind noch einige Konvertierungen nach Ansi-ISO.
    zB.

    val.cStringUsingEncoding_(NSISOLatin1StringEncoding)

    das wäre auch rauszunehmen.

    Grundsätzlich ist das Problem leider nicht so einfach – am einfachsten wäre es wenn du im Adressbuch die Konvertierung schon vorgenommen hättest – sozusagen in der Quelle. Oder alternativ können wir gern auch einen Request bei AVM starten mit der bitte um UTF8-Unterstützung.

  13. thomas sagt:

    ich habe die NSISOLatin2StringEncoding statt der Latin1 gesetzt. Zumindest klappt damit die konvertierung im script. das problem ist später das einspielen in die box die nur iso-8859-1 beherrscht.

  14. Moin,

    gibt es eine Möglichkeit eine Untermenge des Adressbuchs über das Script zu exportieren? Beispielsweise eine bestimmte Adressbuch-Gruppe? Ich habe über 1000 Einträge im OS X Adressbuch und nicht alle brauche ich in der FritzBox, resp. könnte mehrere Submengen in verschiedene Telefonbücher packen (Verwandtschaft, Verein, Firma, …). Über launchd, resp. einem Helferlein namens Lingon habe ich auf apfeltalk.de einen Artikel veröffentlicht: http://www.apfeltalk.de/forum/lingon-gui-f-t98508.html Fragen können wir aber auch gerne bilateral klären.

    Gruß Stefan

  15. Joe sagt:

    Hi Stefan,

    Grundsätzlich “Ja” – Ich gehe davon aus das in irgendeinem Attribut die Gruppe gespeichert ist. Dadurch sollte es möglich sein nur spezifische Gruppen zu exportieren.
    Mein Adressbuch ist nicht so groß – aber sobald ich Zeit finde schaue ich mal was ich rausfinde.
    Joe

  16. Thomas sagt:

    Vielen Dank für diese Script. Spart mir ne menge Arbeit und funktioniert perfekt!!

  17. Joe sagt:

    Hallo Stefan,
    soooo das hat jetzt zwei Fussballspiele zeit gekostet ;-)
    Du musst die Zeile

    people = ab.people()

    entweder durch Variante 1:

    ### Variante 1 zum anzeigen der vorhandenen Gruppe - die letzte Gruppe wird exportiert
    groups = ab.groups()
    for group in groups:
        print group.valueForProperty_(kABUIDProperty),":", group.valueForProperty_(kABGroupNameProperty)
    ###

    oder durch Variante 2:

    ### Variante 2 Nach einer bestimmten Gruppe suchen und sofern nur eine Vorhanden ist - diese exportieren
    suchstring = "MeineGruppe" # Es wird nach einem Substring gesucht
    grsuche = ABGroup.searchElementForProperty_label_key_value_comparison_(kABGroupNameProperty, None , None , suchstring, kABContainsSubStringCaseInsensitive)
    print grsuche
    groups  = ab.recordsMatchingSearchElement_(grsuche)
    for group in groups:
        print group.valueForProperty_(kABUIDProperty),":", group.valueForProperty_(kABGroupNameProperty)
    #####

    ersetzen und anschliessend folgende Zeile ergänzen:

    people = group.members()
  18. Wolf sagt:

    Hallo,

    VIELEN DANK!
    Das ist ein fantastisches Script. Tut genau was es soll! Perfekt!!

    Beim Import ist mir aufgefallen, dass auch Einträge ohne Telefonnummern erstellt werden. In meinem Adressbuch gibt es solche Einträge, also funktioniert das Script wie erwartet – auch wenn das in diesem Fall keinen Sinn macht.
    Wäre es aufwändig, eine Prüfung einzubauen die verhindert, dass Einträge ohne Telefonnummer überhaupt erstellt werden?

    Nachdem ich das “Spitzname”-Feld häufiger verwende, auch für Leute die eher ins Archiv gehören, würde ich wie mazo die Verwendung des “Friend”-Felds vorschlagen. Die Änderung am Python-Code ist ja trivial. Allerdings … finde ich das Feld “Friend” oder “Freund” nicht im Adressbuch…
    Auch wenns peinlich ist, aber welches Feld entspricht denn im Deutschen “kABFriendLabel”?

    Gedanken zu einem automatischen Sync:
    a) Man kann auf dem Mac per launchd automatisch einen Adressbuch-Export zu bestimmten Zeiten erstellen.
    b) Die resultierende Adressdatei könnte dann
    Weg 1) per SMB auf die Fritz!Box (USB-Stick/-Platte)
    oder
    Weg 2) per FTP/WebDAV auf eine Freigabe im Internet
    hinterlegt werden.
    c) Auf der Fritz!Box wird periodisch per cron-Script die Adressdatei
    Weg 1) ins entsprechende Unterverzeichnis verschoben
    Weg 2) heruntergeladen und ins entsprechende Unterverzeichnis verschoben

    Das würde voraussetzen, dass die Datei, die vom Python-Script erzeugt wird, schon dem Format entspricht, das beim Import auf der Fritz!Box generiert wird.
    Geht das?

    Oder gibt es shell-Befehle um das Adressbuch automatisch auf der Fritz!Box zu importieren?

  19. Wolf sagt:

    > …würde ich wie mazo die Verwendung des “Friend”-Felds vorschlagen.
    hätte heissen sollen:
    > …würde ich selbst wie mazo es vorschlägt das “Friend”-Feld nutzen wollen.

    Ich meine also: Ich würde das Script für meine Fälle anpassen. Sicherlich ist das Spitzname-Feld für die meisten Anwender die bessere Wahl.

  20. Wolf sagt:

    Der automatische Export auf dem Mac kann man mit einem launchd-Script realisieren.
    Den Import auf der FRITZ!Box mit pbd, noch nicht automatisch, da der FRITZ!Box ein cron-like Automatismus fehlt, aber auch das lässt sich auf Umwege lösen.

    Eine kleine (!) Anleitung habe ich hier hinterlegt: http://know-how.posterous.com/aktualisierung-von-fritzbox-adressbuch-mit-os
    Und die Scripte dazu hier: http://know-how.posterous.com/fritzbox-adressbuch-scripts

  21. Joe sagt:

    Hi Wolf,
    sehr cool – werd ich mal auftesten.

  22. Wolf sagt:

    Hi Joe,

    der automatische Import ist recht simpel, wenn auf der FRITZ!Box ein SSH-Zugang existiert.
    Dann kann vom Script am Mac aus
    - der Export des Adressbuchs gemacht,
    - das Verschieben auf die FRITZ!Box vorgenommen, und
    - der Import per SSH-Befehl angestossen werden.

  23. Joe sagt:

    Die nächste Version ist in Arbeit und kann zum testen hier:
    http://www.web-dreamer.de/blog/2010/08/mac-adressbuch-fur-avm-fritzbox-version-2-os-x-ipyfritz.html
    gefunden werden…

Hinterlasse eine Antwort