Ejemplo n.º 1
0
class ImpfterminService():
    def __init__(self, code: str, plz: str, kontakt: dict):
        self.code = str(code).upper()
        self.plz = str(plz)
        self.kontakt = kontakt
        self.authorization = b64encode(bytes(f":{code}",
                                             encoding='utf-8')).decode("utf-8")

        # Logging einstellen
        self.log = CLogger("impfterminservice")
        self.log.set_prefix(f"*{self.code[-4:]}")

        # Session erstellen
        self.s = requests.Session()
        self.s.headers.update({
            'Authorization':
            f'Basic {self.authorization}',
            'User-Agent':
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
        })

        # Ausgewähltes Impfzentrum prüfen
        self.verfuegbare_impfzentren = {}
        self.impfzentrum = {}
        self.domain = None
        if not self.impfzentren_laden():
            quit()

        # Verfügbare Impfstoffe laden
        self.verfuegbare_impfstoffe = {}
        if not self.impfstoffe_laden():
            quit()

        # Sonstige
        self.terminpaar = None
        self.qualifikationen = []

    @retry_on_failure()
    def impfzentren_laden(self):
        """Laden aller Impfzentren zum Abgleich der eingegebenen PLZ.

        :return: bool
        """
        url = "https://www.impfterminservice.de/assets/static/impfzentren.json"

        res = self.s.get(url)
        if res.ok:
            # Antwort-JSON umformattieren für einfachere Handhabung
            formattierte_impfzentren = {}
            for bundesland, impfzentren in res.json().items():
                for impfzentrum in impfzentren:
                    formattierte_impfzentren[impfzentrum["PLZ"]] = impfzentrum

            self.verfuegbare_impfzentren = formattierte_impfzentren
            self.log.info(
                f"{len(self.verfuegbare_impfzentren)} Impfzentren verfügbar")

            # Prüfen, ob Impfzentrum zur eingetragenen PLZ existiert
            self.impfzentrum = self.verfuegbare_impfzentren.get(self.plz)
            if self.impfzentrum:
                self.domain = self.impfzentrum.get("URL")
                self.log.info("'{}' in {} {} ausgewählt".format(
                    self.impfzentrum.get("Zentrumsname").strip(),
                    self.impfzentrum.get("PLZ"), self.impfzentrum.get("Ort")))
                return True
            else:
                self.log.error(f"Kein Impfzentrum in PLZ {self.plz} verfügbar")
        else:
            self.log.error("Impfzentren können nicht geladen werden")
        return False

    @retry_on_failure()
    def impfstoffe_laden(self):
        """Laden der verfügbaren Impstoff-Qualifikationen.
        In der Regel gibt es 3 Qualifikationen, die je nach Altersgruppe verteilt werden.

        """
        path = "assets/static/its/vaccination-list.json"
        res = self.s.get(self.domain + path)

        if res.ok:
            res_json = res.json()
            self.log.info(
                f"{len(res_json)} Impfstoffe am Impfzentrum verfügbar")

            for impfstoff in res_json:
                qualifikation = impfstoff.get("qualification")
                name = impfstoff.get("name", "N/A")
                alter = impfstoff.get("age")
                intervall = impfstoff.get("interval")
                self.verfuegbare_impfstoffe[qualifikation] = name
                self.log.info(
                    f"{qualifikation}: {name} --> Altersgruppe: {alter} --> Intervall: {intervall} Tage"
                )
            print(" ")

            return True
        self.log.error(
            "Keine Impfstoffe im ausgewählten Impfzentrum verfügbar")
        return False

    def cookies_erneuern(self):
        """Erneuern des bm_sz Cookies mit Selenium. Dazu wird die Suche-Seite aufgerufen.
        Der Cookie muss alle 10 Minuten oder alle 5 Terminsuche-Requests erneuert werden.

        :return: bool
        """
        # Chromedriver anhand des OS auswählen
        chromedriver = None
        operating_system = platform.system().lower()
        if 'linux' in operating_system:
            chromedriver = "./tools/chromedriver/chromedriver-linux"
        elif 'windows' in operating_system:
            chromedriver = "./tools/chromedriver/chromedriver-windows.exe"
        elif 'darwin' in operating_system:
            if "arm" in platform.processor().lower():
                chromedriver = "./tools/chromedriver/chromedriver-mac-m1"
            else:
                chromedriver = "./tools/chromedriver/chromedriver-mac-intel"

        path = f"impftermine/suche/{self.code}/{self.plz}"
        with Chrome(chromedriver) as driver:
            driver.get(self.domain + path)

            # Aus Erfahrung ist die Cookie-Generierung zuverlässiger,
            # wenn man kurz wartet
            time.sleep(3)

            # bm_sz-Cookie extrahieren und abspeichern
            cookie = driver.get_cookie("bm_sz")
            if cookie:
                self.s.cookies.update(
                    {c['name']: c['value']
                     for c in driver.get_cookies()})
                self.log.info("Cookie generiert: *{}".format(
                    cookie.get("value")[-6:]))
                return True
            else:
                self.log.error("Cookie kann nicht erstellt werden!")
                return False

    @retry_on_failure()
    def login(self):
        """Einloggen mittels Code, um qualifizierte Impfstoffe zu erhalten.
        Dieser Schritt ist wahrscheinlich nicht zwigend notwendig, aber schadet auch nicht.

        :return: bool
        """
        path = f"rest/login?plz={self.plz}"
        res = self.s.get(self.domain + path)
        if res.ok:
            # Checken, welche Impfstoffe für das Alter zur Verfügung stehen
            self.qualifikationen = res.json().get("qualifikationen")
            if self.qualifikationen:
                zugewiesene_impfstoffe = " ".join([
                    self.verfuegbare_impfstoffe.get(q, "N/A")
                    for q in self.qualifikationen
                ])
                self.log.info("Erfolgreich mit Code eingeloggt")
                self.log.info(
                    f"Qualifizierte Impfstoffe: {zugewiesene_impfstoffe}")
                print(" ")

                return True
            else:
                self.log.error("Keine qualifizierten Impfstoffe verfügbar!")
        else:
            self.log.error("Einloggen mit Code nicht möglich!")
        return False

    @retry_on_failure()
    def terminsuche(self):
        """Es wird nach einen verfügbaren Termin in der gewünschten PLZ gesucht.
        Ausgewählt wird der erstbeste Termin (!).
        Zurückgegeben wird das Ergebnis der Abfrage und der Status-Code.
        Bei Status-Code > 400 müssen die Cookies erneuert werden.

        Beispiel für ein Termin-Paar:

        [{
            'slotId': 'slot-56817da7-3f46-4f97-9868-30a6ddabcdef',
            'begin': 1616999901000,
            'bsnr': '005221080'
        }, {
            'slotId': 'slot-d29f5c22-384c-4928-922a-30a6ddabcdef',
            'begin': 1623999901000,
            'bsnr': '005221080'
        }]

        :return: bool, status-code
        """
        path = f"rest/suche/terminpaare?plz={self.plz}"

        res = self.s.get(self.domain + path)
        if res.ok:
            res_json = res.json()

            terminpaare = res_json.get("terminpaare")
            if terminpaare:
                # Auswahl des erstbesten Terminpaares
                self.terminpaar = terminpaare[0]
                self.log.success("Terminpaar gefunden!")

                for num, termin in enumerate(self.terminpaar, 1):
                    ts = datetime.fromtimestamp(
                        termin["begin"] /
                        1000).strftime('%d.%m.%Y um %H:%M Uhr')
                    self.log.success(f"{num}. Termin: {ts}")
                return True, 200
            else:
                self.log.info("Keine Termine verfügbar")
        else:
            self.log.error("Terminpaare können nicht geladen werden")
        return False, res.status_code

    @retry_on_failure()
    def termin_buchen(self):
        """Termin wird gebucht für die Kontaktdaten, die beim Starten des
        Programms eingetragen oder aus der JSON-Datei importiert wurden.

        :return: bool
        """
        path = "rest/buchung"

        # Daten für Impftermin sammeln
        data = {
            "plz":
            self.plz,
            "slots": [
                self.terminpaar[0].get("slotId"),
                self.terminpaar[1].get("slotId")
            ],
            "qualifikationen":
            self.qualifikationen,
            "contact":
            self.kontakt
        }

        res = self.s.post(self.domain + path, json=data)
        if res.status_code == 201:
            self.log.success("Termin erfolgreich gebucht!")
            return True
        else:
            self.log.error("Termin konnte nicht gebucht werden")
            return False

    @staticmethod
    def run(code: str, plz: str, kontakt: json, check_delay: int = 60):
        """Workflow für die Terminbuchung.

        :param code: 14-stelliger Impf-Code
        :param plz: PLZ des Impfzentrums
        :param kontakt: Kontaktdaten der zu impfenden Person als JSON
        :param check_delay: Zeit zwischen Iterationen der Terminsuche
        :return:
        """

        its = ImpfterminService(code, plz, kontakt)
        its.cookies_erneuern()
        while not its.login():
            its.cookies_erneuern()
            time.sleep(3)

        termin_gefunden = False
        while not termin_gefunden:
            termin_gefunden, status_code = its.terminsuche()
            if status_code >= 400:
                its.cookies_erneuern()
            else:
                time.sleep(check_delay)

        its.termin_buchen()
Ejemplo n.º 2
0
class ImpfterminService():
    def __init__(self, code: str, plz_impfzentren: list, kontakt: dict,
                 PATH: str):
        self.code = str(code).upper()
        self.splitted_code = self.code.split("-")

        self.PATH = PATH

        # PLZ's zu String umwandeln
        self.plz_impfzentren = sorted([str(plz) for plz in plz_impfzentren])
        self.plz_termin = None

        self.kontakt = kontakt
        self.authorization = b64encode(bytes(f":{code}",
                                             encoding='utf-8')).decode("utf-8")

        # Logging einstellen
        self.log = CLogger("impfterminservice")
        self.log.set_prefix(
            f"*{self.code[-4:]} | {', '.join(self.plz_impfzentren)}")

        # Session erstellen
        self.s = cloudscraper.create_scraper()
        self.s.headers.update({
            'Authorization': f'Basic {self.authorization}',
            'User-Agent': 'Mozilla/5.0',
        })

        # Ausgewähltes Impfzentrum prüfen
        self.verfuegbare_impfzentren = {}
        self.impfzentrum = {}
        self.domain = None
        if not self.impfzentren_laden():
            raise ValueError("Impfzentren laden fehlgeschlagen")

        # Verfügbare Impfstoffe laden
        self.verfuegbare_qualifikationen: List[Dict] = []
        while not self.impfstoffe_laden():
            self.log.warn("Erneuter Versuch in 60 Sekunden")
            time.sleep(60)

        # OS
        self.operating_system = platform.system().lower()

        # Sonstige
        self.terminpaar = None
        self.qualifikationen = []
        self.app_name = str(self)

    def __str__(self) -> str:
        return "ImpfterminService"

    @retry_on_failure()
    def impfzentren_laden(self):
        """
        Laden aller Impfzentren zum Abgleich der eingegebenen PLZ.

        :return: bool
        """

        url = "https://www.impfterminservice.de/assets/static/impfzentren.json"

        res = self.s.get(url, timeout=15)
        if res.ok:
            # Antwort-JSON umformatieren für einfachere Handhabung
            formatierte_impfzentren = {}
            for bundesland, impfzentren in res.json().items():
                for impfzentrum in impfzentren:
                    formatierte_impfzentren[impfzentrum["PLZ"]] = impfzentrum

            self.verfuegbare_impfzentren = formatierte_impfzentren
            self.log.info(
                f"{len(self.verfuegbare_impfzentren)} Impfzentren verfügbar")

            # Prüfen, ob Impfzentren zur eingetragenen PLZ existieren
            plz_geprueft = []
            for plz in self.plz_impfzentren:
                self.impfzentrum = self.verfuegbare_impfzentren.get(plz)
                if self.impfzentrum:
                    self.domain = self.impfzentrum.get("URL")
                    self.log.info("'{}' in {} {} ausgewählt".format(
                        self.impfzentrum.get("Zentrumsname").strip(),
                        self.impfzentrum.get("PLZ"),
                        self.impfzentrum.get("Ort")))
                    plz_geprueft.append(plz)

            if plz_geprueft:
                self.plz_impfzentren = plz_geprueft
                return True
            else:
                self.log.error(
                    "Kein Impfzentrum zu eingetragenen PLZ's verfügbar.")
                return False
        else:
            self.log.error("Impfzentren können nicht geladen werden")
        return False

    @retry_on_failure(1)
    def impfstoffe_laden(self):
        """
        Laden der verfügbaren Impstoff-Qualifikationen.
        In der Regel gibt es 3 Qualifikationen, die je nach Altersgruppe verteilt werden.

        :return:
        """
        path = "assets/static/its/vaccination-list.json"

        res = self.s.get(self.domain + path, timeout=15)
        if res.ok:
            res_json = res.json()

            for qualifikation in res_json:
                qualifikation["impfstoffe"] = qualifikation.get(
                    "tssname", "N/A").replace(" ", "").split(",")
                self.verfuegbare_qualifikationen.append(qualifikation)

            # Ausgabe der verfügbaren Impfstoffe:
            for qualifikation in self.verfuegbare_qualifikationen:
                q_id = qualifikation["qualification"]
                alter = qualifikation.get("age", "N/A")
                intervall = qualifikation.get("interval", " ?")
                impfstoffe = str(qualifikation["impfstoffe"])
                self.log.info(
                    f"[{q_id}] Altersgruppe: {alter} (Intervall: {intervall} Tage) --> {impfstoffe}"
                )
            print("")
            return True

        self.log.error(
            "Keine Impfstoffe im ausgewählten Impfzentrum verfügbar")
        return False

    def get_chromedriver_path(self):
        """
        :return: String mit Pfad zur chromedriver-Programmdatei
        """
        chromedriver_from_env = os.getenv("VACCIPY_CHROMEDRIVER")
        if chromedriver_from_env:
            return chromedriver_from_env

        # Chromedriver anhand des OS auswählen
        if 'linux' in self.operating_system:
            if "64" in platform.architecture() or sys.maxsize > 2**32:
                return os.path.join(
                    self.PATH, "tools/chromedriver/chromedriver-linux-64")
            else:
                return os.path.join(
                    self.PATH, "tools/chromedriver/chromedriver-linux-32")
        elif 'windows' in self.operating_system:
            return os.path.join(self.PATH,
                                "tools/chromedriver/chromedriver-windows.exe")
        elif 'darwin' in self.operating_system:
            if "arm" in platform.processor().lower():
                return os.path.join(self.PATH,
                                    "tools/chromedriver/chromedriver-mac-m1")
            else:
                return os.path.join(
                    self.PATH, "tools/chromedriver/chromedriver-mac-intel")
        else:
            raise ValueError(
                f"Nicht unterstütztes Betriebssystem {self.operating_system}")

    def get_chromedriver(self, headless):
        chrome_options = Options()

        # deaktiviere Selenium Logging
        chrome_options.add_argument('disable-infobars')
        chrome_options.add_experimental_option('useAutomationExtension', False)
        chrome_options.add_experimental_option("excludeSwitches",
                                               ["enable-automation"])
        chrome_options.add_experimental_option('excludeSwitches',
                                               ['enable-logging'])

        # Chrome head is only required for the backup booking process.
        # User-Agent is required for headless, because otherwise the server lets us hang.
        chrome_options.add_argument("user-agent=Mozilla/5.0")

        chrome_options.headless = headless

        return Chrome(self.get_chromedriver_path(), options=chrome_options)

    def driver_enter_code(self, driver, plz_impfzentrum):
        """
        TODO xpath code auslagern
        """
        url = f"{self.domain}impftermine/service?plz={plz_impfzentrum}"

        driver.get(url)

        # Queue Bypass
        queue_cookie = driver.get_cookie("akavpwr_User_allowed")
        if queue_cookie:
            self.log.info("Im Warteraum, Seite neuladen")
            queue_cookie["name"] = "akavpau_User_allowed"
            driver.add_cookie(queue_cookie)

            # Seite neu laden
            driver.get(url)
            driver.refresh()

        # Klick auf "Auswahl bestätigen" im Cookies-Banner
        # Warteraum-Support: Timeout auf 1 Stunde
        button_xpath = ".//html/body/app-root/div/div/div/div[2]/div[2]/div/div[1]/a"
        button = WebDriverWait(driver, 60 * 60).until(
            EC.element_to_be_clickable((By.XPATH, button_xpath)))
        action = ActionChains(driver)
        action.move_to_element(button).click().perform()

        # Klick auf "Vermittlungscode bereits vorhanden"
        button_xpath = "/html/body/app-root/div/app-page-its-login/div/div/div[2]/app-its-login-user/" \
                        "div/div/app-corona-vaccination/div[2]/div/div/label[1]/span"
        button = WebDriverWait(driver, 1).until(
            EC.element_to_be_clickable((By.XPATH, button_xpath)))
        action = ActionChains(driver)
        action.move_to_element(button).click().perform()

        # Auswahl des ersten Code-Input-Feldes
        input_xpath = "/html/body/app-root/div/app-page-its-login/div/div/div[2]/app-its-login-user/" \
                        "div/div/app-corona-vaccination/div[3]/div/div/div/div[1]/app-corona-vaccination-yes/" \
                        "form[1]/div[1]/label/app-ets-input-code/div/div[1]/label/input"
        input_field = WebDriverWait(driver, 1).until(
            EC.element_to_be_clickable((By.XPATH, input_xpath)))
        action = ActionChains(driver)
        action.move_to_element(input_field).click().perform()

        # Code eintragen
        input_field.send_keys(self.code)
        time.sleep(.1)

        # Klick auf "Termin suchen"
        button_xpath = "/html/body/app-root/div/app-page-its-login/div/div/div[2]/app-its-login-user/" \
                        "div/div/app-corona-vaccination/div[3]/div/div/div/div[1]/app-corona-vaccination-yes/" \
                        "form[1]/div[2]/button"
        button = WebDriverWait(driver, 1).until(
            EC.element_to_be_clickable((By.XPATH, button_xpath)))
        action = ActionChains(driver)
        action.move_to_element(button).click().perform()

        # Maus-Bewegung hinzufügen (nicht sichtbar)
        action.move_by_offset(10, 20).perform()

    def driver_renew_cookies(self, driver, plz_impfzentrum):
        self.driver_enter_code(driver, plz_impfzentrum)

        # prüfen, ob Cookies gesetzt wurden und in Session übernehmen
        try:
            cookie = driver.get_cookie("bm_sz")
            if cookie:
                self.s.cookies.clear()
                self.s.cookies.update(
                    {c['name']: c['value']
                     for c in driver.get_cookies()})
                self.log.info("Browser-Cookie generiert: *{}".format(
                    cookie.get("value")[-6:]))
                return True
            else:
                self.log.error("Cookies können nicht erstellt werden!")
                return False
        except:
            return False

    def driver_book_appointment(self, driver, plz_impfzentrum):
        url = f"{self.domain}impftermine/service?plz={plz_impfzentrum}"

        self.driver_enter_code(driver, plz_impfzentrum)

        try:
            # Klick auf "Termin suchen"
            button_xpath = "/html/body/app-root/div/app-page-its-search/div/div/div[2]/div/div/div[5]/div/div[1]/div[2]/div[2]/button"
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.5)
        except:
            self.log.error("Termine können nicht gesucht werden")
            pass

        # Termin auswählen
        try:
            button_xpath = '//*[@id="itsSearchAppointmentsModal"]/div/div/div[2]/div/div/form/div[1]/div[2]/label/div[2]/div'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.5)
        except:
            self.log.error("Termine können nicht ausgewählt werden")
            pass

        # Klick Button "AUSWÄHLEN"
        try:
            button_xpath = '//*[@id="itsSearchAppointmentsModal"]/div/div/div[2]/div/div/form/div[2]/button[1]'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.5)
        except:
            self.log.error("Termine können nicht ausgewählt werden (Button)")
            pass

        # Klick Daten erfassen
        try:
            button_xpath = '/html/body/app-root/div/app-page-its-search/div/div/div[2]/div/div/div[5]/div/div[2]/div[2]/div[2]/button'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.5)
        except:
            self.log.error("1. Daten können nicht erfasst werden")
            pass
        try:
            # Klick Anrede
            if self.kontakt['anrede'] == "Herr":
                button_xpath = '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[1]/div/div/div/label[1]/span'
            elif self.kontakt['anrede'] == "Frau":
                button_xpath = '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[1]/div/div/div/label[2]/span'
            else:
                button_xpath = '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[1]/div/div/div/label[3]/span'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()

            # Input Vorname
            input_xpath = '/html/body/app-root/div/app-page-its-search/app-its-search-contact-modal/div/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[2]/div[1]/div/label/input'
            input_field = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, input_xpath)))
            action.move_to_element(input_field).click().perform()
            input_field.send_keys(self.kontakt['vorname'])

            # Input Nachname
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[2]/div[2]/div/label/input'
            )
            input_field.send_keys(self.kontakt['nachname'])

            # Input PLZ
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[3]/div[1]/div/label/input'
            )
            input_field.send_keys(self.kontakt['plz'])

            # Input City
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[3]/div[2]/div/label/input'
            )
            input_field.send_keys(self.kontakt['ort'])

            # Input Strasse
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[4]/div[1]/div/label/input'
            )
            input_field.send_keys(self.kontakt['strasse'])

            # Input Hasunummer
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[4]/div[2]/div/label/input'
            )
            input_field.send_keys(self.kontakt['hausnummer'])

            # Input Telefonnummer
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[4]/div[3]/div/label/div/input'
            )
            input_field.send_keys(self.kontakt['phone'].replace("+49", ""))

            # Input Mail
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[1]/app-booking-contact-form/div[5]/div/div/label/input'
            )
            input_field.send_keys(self.kontakt['notificationReceiver'])
        except:
            self.log.error("Kontaktdaten können nicht eingegeben werden")
            pass

        # Klick Button "ÜBERNEHMEN"
        try:
            button_xpath = '//*[@id="itsSearchContactModal"]/div/div/div[2]/div/form/div[2]/button[1]'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.7)
        except:
            self.log.error("Button ÜBERNEHMEN kann nicht gedrückt werden")
            pass

        # Termin buchen
        try:
            button_xpath = '/html/body/app-root/div/app-page-its-search/div/div/div[2]/div/div/div[5]/div/div[3]/div[2]/div[2]/button'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
        except:
            self.log.error("Button Termin buchen kann nicht gedrückt werden")
            pass
        time.sleep(3)
        if "Ihr Termin am" in str(driver.page_source):
            msg = "Termin erfolgreich gebucht!"
            self.log.success(msg)
            desktop_notification(operating_system=self.operating_system,
                                 title="Terminbuchung:",
                                 message=msg)
            return True
        else:
            self.log.error(
                "Automatisierte Terminbuchung fehlgeschlagen. Termin manuell im Fenster oder im Browser buchen."
            )
            print("Link für manuelle Buchung im Browser:", url)
            time.sleep(10 * 60)
            return False

    @retry_on_failure()
    def renew_cookies(self):
        """
        Cookies der Session erneuern, wenn sie abgelaufen sind.
        :return:
        """

        self.log.info("Browser-Cookies generieren")
        with self.get_chromedriver(headless=True) as driver:
            return self.driver_renew_cookies(driver,
                                             choice(self.plz_impfzentren))

    @retry_on_failure()
    def book_appointment(self):
        """
        Backup Prozess:
        Wenn die Terminbuchung mit dem Bot nicht klappt, wird das
        Browserfenster geöffnet und die Buchung im Browser beendet
        :return:
        """

        self.log.info("Termin über Selenium buchen")
        with self.get_chromedriver(headless=False) as driver:
            return self.driver_book_appointment(driver, self.plz_termin)

    @retry_on_failure()
    def login(self):
        """Einloggen mittels Code, um qualifizierte Impfstoffe zu erhalten.
        Dieser Schritt ist wahrscheinlich nicht zwingend notwendig, aber schadet auch nicht.

        :return: bool
        """

        path = f"rest/login?plz={choice(self.plz_impfzentren)}"

        res = self.s.get(self.domain + path, timeout=15)
        if res.ok:
            # Checken, welche Impfstoffe für das Alter zur Verfügung stehen
            self.qualifikationen = res.json().get("qualifikationen")

            if self.qualifikationen:
                zugewiesene_impfstoffe = set()

                for q in self.qualifikationen:
                    for verfuegbare_q in self.verfuegbare_qualifikationen:
                        if verfuegbare_q["qualification"] == q:
                            zugewiesene_impfstoffe.update(
                                verfuegbare_q["impfstoffe"])

                self.log.info("Erfolgreich mit Code eingeloggt")
                self.log.info(
                    f"Mögliche Impfstoffe: {list(zugewiesene_impfstoffe)}")
                print(" ")

                return True
            else:
                self.log.warn("Keine qualifizierten Impfstoffe verfügbar")
        else:
            return False

    @retry_on_failure()
    def termin_suchen(self, plz):
        """Es wird nach einen verfügbaren Termin in der gewünschten PLZ gesucht.
        Ausgewählt wird der erstbeste Termin (!).
        Zurückgegeben wird das Ergebnis der Abfrage und der Status-Code.
        Bei Status-Code > 400 müssen die Cookies erneuert werden.

        Beispiel für ein Termin-Paar:

        [{
            'slotId': 'slot-56817da7-3f46-4f97-9868-30a6ddabcdef',
            'begin': 1616999901000,
            'bsnr': '005221080'
        }, {
            'slotId': 'slot-d29f5c22-384c-4928-922a-30a6ddabcdef',
            'begin': 1623999901000,
            'bsnr': '005221080'
        }]

        :return: bool, status-code
        """

        path = f"rest/suche/impfterminsuche?plz={plz}"

        while True:
            res = self.s.get(self.domain + path, timeout=15)
            if not res.ok or 'Virtueller Warteraum des Impfterminservice' not in res.text:
                break
            self.log.info('Warteraum... zZz...')
            time.sleep(30)

        if res.ok:
            res_json = res.json()
            terminpaare = res_json.get("termine")
            if terminpaare:
                # Auswahl des erstbesten Terminpaares
                self.terminpaar = choice(terminpaare)
                self.plz_termin = plz
                self.log.success(f"Terminpaar gefunden!")
                self.impfzentrum = self.verfuegbare_impfzentren.get(plz)
                self.log.success("'{}' in {} {}".format(
                    self.impfzentrum.get("Zentrumsname").strip(),
                    self.impfzentrum.get("PLZ"), self.impfzentrum.get("Ort")))
                for num, termin in enumerate(self.terminpaar, 1):
                    ts = datetime.fromtimestamp(
                        termin["begin"] /
                        1000).strftime('%d.%m.%Y um %H:%M Uhr')
                    self.log.success(f"{num}. Termin: {ts}")
                if ENABLE_BEEPY:
                    beepy.beep('coin')
                return True, 200
            else:
                self.log.info(f"Keine Termine verfügbar in {plz}")
        else:
            self.log.error(
                f"Terminpaare können nicht geladen werden: {res.text}")
        return False, res.status_code

    @retry_on_failure()
    def termin_buchen(self):
        """Termin wird gebucht für die Kontaktdaten, die beim Starten des
        Programms eingetragen oder aus der JSON-Datei importiert wurden.

        :return: bool
        """

        path = "rest/buchung"

        # Daten für Impftermin sammeln
        data = {
            "plz": self.plz_termin,
            "slots": [termin.get("slotId") for termin in self.terminpaar],
            "qualifikationen": self.qualifikationen,
            "contact": self.kontakt
        }

        res = self.s.post(self.domain + path, json=data, timeout=15)

        if res.status_code == 201:
            msg = "Termin erfolgreich gebucht!"
            self.log.success(msg)
            desktop_notification(operating_system=self.operating_system,
                                 title="Terminbuchung:",
                                 message=msg)
            return True

        elif res.status_code == 429:
            msg = "Anfrage wurde von der Botprotection geblockt."
        elif res.status_code >= 400:
            data = res.json()
            try:
                error = data['errors']['status']
            except KeyError:
                error = ''
            if 'nicht mehr verfügbar' in error:
                msg = f"Diesen Termin gibts nicht mehr: {error}"
            else:
                msg = f"Termin konnte nicht gebucht werden: {data}"
        else:
            msg = f"Unbekannter Statuscode: {res.status_code}"

        self.log.error(msg)
        desktop_notification(operating_system=self.operating_system,
                             title="Terminbuchung:",
                             message=msg)
        return False

    @retry_on_failure()
    def code_anfordern(self, mail, telefonnummer, plz_impfzentrum,
                       leistungsmerkmal):
        """
        SMS-Code beim Impfterminservice anfordern.

        :param mail: Mail für Empfang des Codes
        :param telefonnummer: Telefonnummer für SMS-Code
        :param plz_impfzentrum: PLZ des Impfzentrums, für das ein Code erstellt werden soll
        :param leistungsmerkmal: gewählte Impfgruppe (bspw. L921)
        :return:
        """

        path = "rest/smspin/anforderung"

        data = {
            "email": mail,
            "leistungsmerkmal": leistungsmerkmal,
            "phone": "+49" + telefonnummer,
            "plz": plz_impfzentrum
        }
        while True:
            res = self.s.post(self.domain + path, json=data, timeout=15)
            if res.ok:
                token = res.json().get("token")
                return token
            elif res.status_code == 429:
                self.log.error(
                    "Anfrage wurde von der Botprotection geblockt.\n"
                    "Es werden manuelle Cookies aus dem Browser benötigt.\n"
                    "Bitte Anleitung im FAQ in GitHub beachten.\n"
                    f"Link: {self.domain}impftermine/service?plz={plz_impfzentrum}"
                )
                cookies = input("> Manuelle Cookies: ").strip()
                optional_prefix = "Cookie: "
                if cookies.startswith(optional_prefix):
                    cookies = cookies[len(optional_prefix):]
                self.s.headers.update({'Cookie': cookies})

            else:
                self.log.error(f"Code kann nicht angefragt werden: {res.text}")
                return None

    @retry_on_failure()
    def code_bestaetigen(self, token, sms_pin):
        """
        Bestätigung der Code-Generierung mittels SMS-Code

        :param token: Token der Code-Erstellung
        :param sms_pin: 6-stelliger SMS-Code
        :return:
        """

        path = f"rest/smspin/verifikation"
        data = {"token": token, "smspin": sms_pin}
        res = self.s.post(self.domain + path, json=data, timeout=15)
        if res.ok:
            self.log.success(
                "Der Impf-Code wurde erfolgreich angefragt, bitte prüfe deine Mails!"
            )
            return True
        else:
            self.log.error(f"Code-Verifikation fehlgeschlagen: {res.text}")
            return False

    @staticmethod
    def terminsuche(code: str,
                    plz_impfzentren: list,
                    kontakt: dict,
                    PATH: str,
                    check_delay: int = 30):
        """
        Workflow für die Terminbuchung.

        :param code: 14-stelliger Impf-Code
        :param plz_impfzentren: Liste mit PLZ von Impfzentren
        :param kontakt: Kontaktdaten der zu impfenden Person als JSON
        :param check_delay: Zeit zwischen Iterationen der Terminsuche
        :return:
        """

        its = ImpfterminService(code, plz_impfzentren, kontakt, PATH)
        its.renew_cookies()

        # login ist nicht zwingend erforderlich
        its.login()

        while True:
            termin_gefunden = False
            while not termin_gefunden:

                # durchlaufe jede eingegebene PLZ und suche nach Termin
                for plz in its.plz_impfzentren:
                    termin_gefunden, status_code = its.termin_suchen(plz)

                    # Durchlauf aller PLZ unterbrechen, wenn Termin gefunden wurde
                    if termin_gefunden:
                        break
                    # Cookies erneuern
                    elif status_code >= 400:
                        its.renew_cookies()
                    # Suche pausieren
                    if not termin_gefunden:
                        time.sleep(check_delay)

            # Programm beenden, wenn Termin gefunden wurde
            if its.termin_buchen():
                return True

            # Cookies erneuern und pausieren, wenn Terminbuchung nicht möglich war
            # Anschließend nach neuem Termin suchen
            if its.book_appointment():
                return True
Ejemplo n.º 3
0
class ImpfterminService():
    def __init__(self, code: str, plz: str, kontakt: dict):
        self.code = str(code).upper()
        self.splitted_code = self.code.split("-")

        self.plz = str(plz)
        self.kontakt = kontakt
        self.authorization = b64encode(bytes(f":{code}",
                                             encoding='utf-8')).decode("utf-8")

        # Logging einstellen
        self.log = CLogger("impfterminservice")
        self.log.set_prefix(f"*{self.code[-4:]} | {self.plz}")

        # Session erstellen
        self.s = requests.Session()
        self.s.headers.update({
            'Authorization':
            f'Basic {self.authorization}',
            'User-Agent':
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
        })

        # Ausgewähltes Impfzentrum prüfen
        self.verfuegbare_impfzentren = {}
        self.impfzentrum = {}
        self.domain = None
        if not self.impfzentren_laden():
            quit()

        # Verfügbare Impfstoffe laden
        self.verfuegbare_qualifikationen: List[Dict] = []
        while not self.impfstoffe_laden():
            self.log.warn("Erneuter Versuch in 60 Sekunden")
            time.sleep(60)

        # OS
        self.operating_system = platform.system().lower()

        # Sonstige
        self.terminpaar = None
        self.qualifikationen = []
        self.app_name = str(self)

    def __str__(self) -> str:
        return "ImpfterminService"

    @retry_on_failure()
    def impfzentren_laden(self):
        """Laden aller Impfzentren zum Abgleich der eingegebenen PLZ.

        :return: bool
        """
        url = "https://www.impfterminservice.de/assets/static/impfzentren.json"

        res = self.s.get(url, timeout=15)
        if res.ok:
            # Antwort-JSON umformatieren für einfachere Handhabung
            formatierte_impfzentren = {}
            for bundesland, impfzentren in res.json().items():
                for impfzentrum in impfzentren:
                    formatierte_impfzentren[impfzentrum["PLZ"]] = impfzentrum

            self.verfuegbare_impfzentren = formatierte_impfzentren
            self.log.info(
                f"{len(self.verfuegbare_impfzentren)} Impfzentren verfügbar")

            # Prüfen, ob Impfzentrum zur eingetragenen PLZ existiert
            self.impfzentrum = self.verfuegbare_impfzentren.get(self.plz)
            if self.impfzentrum:
                self.domain = self.impfzentrum.get("URL")
                self.log.info("'{}' in {} {} ausgewählt".format(
                    self.impfzentrum.get("Zentrumsname").strip(),
                    self.impfzentrum.get("PLZ"), self.impfzentrum.get("Ort")))
                return True
            else:
                self.log.error(f"Kein Impfzentrum in PLZ {self.plz} verfügbar")
        else:
            self.log.error("Impfzentren können nicht geladen werden")
        return False

    @retry_on_failure(1)
    def impfstoffe_laden(self):
        """Laden der verfügbaren Impstoff-Qualifikationen.
        In der Regel gibt es 3 Qualifikationen, die je nach Altersgruppe verteilt werden.

        """

        path = "assets/static/its/vaccination-list.json"

        res = self.s.get(self.domain + path, timeout=15)
        if res.ok:
            res_json = res.json()

            for qualifikation in res_json:
                qualifikation["impfstoffe"] = qualifikation.get(
                    "tssname", "N/A").replace(" ", "").split(",")
                self.verfuegbare_qualifikationen.append(qualifikation)

            # Ausgabe der verfügbaren Impfstoffe:
            for qualifikation in self.verfuegbare_qualifikationen:
                q_id = qualifikation["qualification"]
                alter = qualifikation.get("age", "N/A")
                intervall = qualifikation.get("interval", " ?")
                impfstoffe = str(qualifikation["impfstoffe"])
                self.log.info(
                    f"[{q_id}] Altersgruppe: {alter} (Intervall: {intervall} Tage) --> {impfstoffe}"
                )
            print("\n")
            return True

        self.log.error(
            "Keine Impfstoffe im ausgewählten Impfzentrum verfügbar")
        return False

    @retry_on_failure()
    def cookies_erneuern(self):
        self.log.info("Browser-Cookies generieren")
        # Chromedriver anhand des OS auswählen
        chromedriver = None
        if 'linux' in self.operating_system:
            chromedriver = os.path.join(
                PATH, "tools/chromedriver/chromedriver-linux")
        elif 'windows' in self.operating_system:
            chromedriver = os.path.join(
                PATH, "tools/chromedriver/chromedriver-windows.exe")
        elif 'darwin' in self.operating_system:
            if "arm" in platform.processor().lower():
                chromedriver = os.path.join(
                    PATH, "tools/chromedriver/chromedriver-mac-m1")
            else:
                chromedriver = os.path.join(
                    PATH, "tools/chromedriver/chromedriver-mac-intel")

        path = "impftermine/service?plz={}".format(self.plz)

        with Chrome(chromedriver) as driver:
            driver.get(self.domain + path)

            # Queue Bypass
            queue_cookie = driver.get_cookie("akavpwr_User_allowed")
            if queue_cookie:
                self.log.info("Im Warteraum, Seite neuladen")
                queue_cookie["name"] = "akavpau_User_allowed"
                driver.add_cookie(queue_cookie)

                # Seite neu laden
                driver.get(self.domain + path)
                driver.refresh()

            # Klick auf "Auswahl bestätigen" im Cookies-Banner
            # Warteraum-Support: Timeout auf 1 Stunde
            button_xpath = ".//html/body/app-root/div/div/div/div[2]/div[2]/div/div[1]/a"
            button = WebDriverWait(driver, 60 * 60).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()

            # Klick auf "Vermittlungscode bereits vorhanden"
            button_xpath = "/html/body/app-root/div/app-page-its-login/div/div/div[2]/app-its-login-user/" \
                           "div/div/app-corona-vaccination/div[2]/div/div/label[1]/span"
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()

            # Auswahl des ersten Code-Input-Feldes
            input_xpath = "/html/body/app-root/div/app-page-its-login/div/div/div[2]/app-its-login-user/" \
                          "div/div/app-corona-vaccination/div[3]/div/div/div/div[1]/app-corona-vaccination-yes/" \
                          "form[1]/div[1]/label/app-ets-input-code/div/div[1]/label/input"
            input_field = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, input_xpath)))
            action = ActionChains(driver)
            action.move_to_element(input_field).click().perform()

            # Code eintragen
            input_field.send_keys(self.code)
            time.sleep(.1)

            # Klick auf "Termin suchen"
            button_xpath = "/html/body/app-root/div/app-page-its-login/div/div/div[2]/app-its-login-user/" \
                           "div/div/app-corona-vaccination/div[3]/div/div/div/div[1]/app-corona-vaccination-yes/" \
                           "form[1]/div[2]/button"
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()

            # Maus-Bewegung hinzufügen (nicht sichtbar)
            action.move_by_offset(10, 20).perform()

            # prüfen, ob Cookies gesetzt wurden und in Session übernehmen
            try:
                cookie = driver.get_cookie("bm_sz")
                if cookie:
                    self.s.cookies.clear()
                    self.s.cookies.update(
                        {c['name']: c['value']
                         for c in driver.get_cookies()})
                    self.log.info("Browser-Cookie generiert: *{}".format(
                        cookie.get("value")[-6:]))
                    return True
                else:
                    self.log.error("Cookies können nicht erstellt werden!")
                    return False
            except:
                return False

    @retry_on_failure()
    def login(self):
        """Einloggen mittels Code, um qualifizierte Impfstoffe zu erhalten.
        Dieser Schritt ist wahrscheinlich nicht zwingend notwendig, aber schadet auch nicht.

        :return: bool
        """
        path = f"rest/login?plz={self.plz}"

        res = self.s.get(self.domain + path, timeout=15)
        if res.ok:
            # Checken, welche Impfstoffe für das Alter zur Verfügung stehen
            self.qualifikationen = res.json().get("qualifikationen")

            if self.qualifikationen:
                zugewiesene_impfstoffe = set()

                for q in self.qualifikationen:
                    for verfuegbare_q in self.verfuegbare_qualifikationen:
                        if verfuegbare_q["qualification"] == q:
                            zugewiesene_impfstoffe.update(
                                verfuegbare_q["impfstoffe"])

                self.log.info("Erfolgreich mit Code eingeloggt")
                self.log.info(
                    f"Mögliche Impfstoffe: {list(zugewiesene_impfstoffe)}")
                print(" ")

                return True
            else:
                self.log.warn("Keine qualifizierten Impfstoffe verfügbar")
        else:
            self.log.warn("Einloggen mit Code nicht möglich")
        print(" ")
        return False

    @retry_on_failure()
    def terminsuche(self):
        """Es wird nach einen verfügbaren Termin in der gewünschten PLZ gesucht.
        Ausgewählt wird der erstbeste Termin (!).
        Zurückgegeben wird das Ergebnis der Abfrage und der Status-Code.
        Bei Status-Code > 400 müssen die Cookies erneuert werden.

        Beispiel für ein Termin-Paar:

        [{
            'slotId': 'slot-56817da7-3f46-4f97-9868-30a6ddabcdef',
            'begin': 1616999901000,
            'bsnr': '005221080'
        }, {
            'slotId': 'slot-d29f5c22-384c-4928-922a-30a6ddabcdef',
            'begin': 1623999901000,
            'bsnr': '005221080'
        }]

        :return: bool, status-code
        """

        path = f"rest/suche/impfterminsuche?plz={self.plz}"

        while True:
            res = self.s.get(self.domain + path, timeout=15)
            if not res.ok or 'Virtueller Warteraum des Impfterminservice' not in res.text:
                break
            self.log.info('Warteraum... zZz...')
            time.sleep(30)

        if res.ok:
            res_json = res.json()
            terminpaare = res_json.get("termine")
            if terminpaare:
                # Auswahl des erstbesten Terminpaares
                self.terminpaar = choice(terminpaare)
                self.log.success("Terminpaar gefunden!")

                for num, termin in enumerate(self.terminpaar, 1):
                    ts = datetime.fromtimestamp(
                        termin["begin"] /
                        1000).strftime('%d.%m.%Y um %H:%M Uhr')
                    self.log.success(f"{num}. Termin: {ts}")
                return True, 200
            else:
                self.log.info("Keine Termine verfügbar")
        else:
            self.log.error("Terminpaare können nicht geladen werden")
        return False, res.status_code

    @retry_on_failure()
    def termin_buchen(self):
        """Termin wird gebucht für die Kontaktdaten, die beim Starten des
        Programms eingetragen oder aus der JSON-Datei importiert wurden.

        :return: bool
        """

        path = "rest/buchung"

        # Daten für Impftermin sammeln
        data = {
            "plz": self.plz,
            "slots": [termin.get("slotId") for termin in self.terminpaar],
            "qualifikationen": self.qualifikationen,
            "contact": self.kontakt
        }

        res = self.s.post(self.domain + path, json=data, timeout=15)
        if res.status_code == 201:
            msg = "Termin erfolgreich gebucht!"
            self.log.success(msg)
            self._desktop_notification("Terminbuchung:", msg)
            return True
        else:
            data = res.json()
            try:
                error = data['errors']['status']
            except KeyError:
                error = ''
            if 'nicht mehr verfügbar' in error:
                msg = f"Diesen Termin gibts nicht mehr: {error}"
                self.log.error(msg)
                self._desktop_notification("Terminbuchung:", msg)
            else:
                msg = f"Termin konnte nicht gebucht werden: {data}"
                self.log.error(msg)
                self._desktop_notification("Terminbuchung:", msg)
            return False

    @staticmethod
    def run(code: str, plz: str, kontakt: json, check_delay: int = 60):
        """Workflow für die Terminbuchung.

        :param code: 14-stelliger Impf-Code
        :param plz: PLZ des Impfzentrums
        :param kontakt: Kontaktdaten der zu impfenden Person als JSON
        :param check_delay: Zeit zwischen Iterationen der Terminsuche
        :return:
        """

        its = ImpfterminService(code, plz, kontakt)
        its.cookies_erneuern()

        # login ist nicht zwingend erforderlich
        its.login()

        while True:
            termin_gefunden = False
            while not termin_gefunden:
                termin_gefunden, status_code = its.terminsuche()
                if status_code >= 400:
                    its.cookies_erneuern()
                elif not termin_gefunden:
                    time.sleep(check_delay)

            if its.termin_buchen():
                break
            time.sleep(30)

    def _desktop_notification(self, title: str, message: str):
        """
        Starts a thread and creates a desktop notification using plyer.notification
        """

        if 'windows' not in self.operating_system:
            return

        try:
            Thread(target=notification.notify(
                app_name=self.app_name, title=title, message=message)).start()
        except Exception as exc:
            self.log.error("Error in _desktop_notification: " +
                           str(exc.__class__.__name__) +
                           traceback.format_exc())
Ejemplo n.º 4
0
class ImpfterminService():
    def __init__(self, code: str, plz_impfzentren: list, kontakt: dict,
                 PATH: str):
        self.code = str(code).upper()
        self.splitted_code = self.code.split("-")

        self.PATH = PATH

        # PLZ's zu String umwandeln
        self.plz_impfzentren = sorted([str(plz) for plz in plz_impfzentren])
        self.plz_termin = None

        self.kontakt = kontakt
        self.authorization = b64encode(bytes(f":{code}",
                                             encoding='utf-8')).decode("utf-8")

        # Logging einstellen
        self.log = CLogger("impfterminservice")
        self.log.set_prefix(
            f"*{self.code[-4:]} | {', '.join(self.plz_impfzentren)}")

        # Session erstellen
        self.s = cloudscraper.create_scraper()
        self.s.headers.update({
            'Authorization': f'Basic {self.authorization}',
            'User-Agent': 'Mozilla/5.0',
        })

        # Ausgewähltes Impfzentrum prüfen
        self.verfuegbare_impfzentren = {}
        self.domain = None
        if not self.impfzentren_laden():
            raise ValueError("Impfzentren laden fehlgeschlagen")

        # Verfügbare Impfstoffe laden
        self.verfuegbare_qualifikationen: List[Dict] = []
        while not self.impfstoffe_laden():
            self.log.warn("Erneuter Versuch in 60 Sekunden")
            time.sleep(60)

        # OS
        self.operating_system = platform.system().lower()

        # Sonstige
        self.terminpaar = None
        self.qualifikationen = []
        self.app_name = str(self)

    def __str__(self) -> str:
        return "ImpfterminService"

    @retry_on_failure()
    def impfzentren_laden(self):
        """
        Laden aller Impfzentren zum Abgleich der eingegebenen PLZ.

        :return: bool
        """

        url = "https://www.impfterminservice.de/assets/static/impfzentren.json"

        res = self.s.get(url, timeout=15)
        if res.ok:
            # Antwort-JSON umformatieren für einfachere Handhabung
            formatierte_impfzentren = {}
            for bundesland, impfzentren in res.json().items():
                for impfzentrum in impfzentren:
                    formatierte_impfzentren[impfzentrum["PLZ"]] = impfzentrum

            self.verfuegbare_impfzentren = formatierte_impfzentren
            self.log.info(
                f"{len(self.verfuegbare_impfzentren)} Impfzentren verfügbar")

            # Prüfen, ob Impfzentren zur eingetragenen PLZ existieren
            plz_geprueft = []
            for plz in self.plz_impfzentren:
                impfzentrum = self.verfuegbare_impfzentren.get(plz)
                if impfzentrum:
                    self.domain = impfzentrum.get("URL")
                    zentrumsname = impfzentrum.get("Zentrumsname")
                    ort = impfzentrum.get("Ort")
                    self.log.info(
                        f"'{zentrumsname}' in {plz} {ort} ausgewählt")
                    plz_geprueft.append(plz)

            if plz_geprueft:
                self.plz_impfzentren = plz_geprueft
                return True
            else:
                self.log.error(
                    "Kein Impfzentrum zu eingetragenen PLZ's verfügbar.")
                return False
        else:
            self.log.error("Impfzentren können nicht geladen werden")
        return False

    @retry_on_failure(1)
    def impfstoffe_laden(self):
        """
        Laden der verfügbaren Impstoff-Qualifikationen.
        In der Regel gibt es 3 Qualifikationen, die je nach Altersgruppe verteilt werden.

        :return:
        """
        path = "assets/static/its/vaccination-list.json"

        res = self.s.get(self.domain + path, timeout=15)
        if res.ok:
            res_json = res.json()

            for qualifikation in res_json:
                qualifikation["impfstoffe"] = qualifikation.get(
                    "tssname", "N/A").replace(" ", "").split(",")
                self.verfuegbare_qualifikationen.append(qualifikation)

            # Ausgabe der verfügbaren Impfstoffe:
            for qualifikation in self.verfuegbare_qualifikationen:
                q_id = qualifikation["qualification"]
                alter = qualifikation.get("age", "N/A")
                intervall = qualifikation.get("interval", " ?")
                impfstoffe = str(qualifikation["impfstoffe"])
                self.log.info(
                    f"[{q_id}] Altersgruppe: {alter} (Intervall: {intervall} Tage) --> {impfstoffe}"
                )
            print("")
            return True

        self.log.error(
            "Keine Impfstoffe im ausgewählten Impfzentrum verfügbar")
        return False

    def get_chromedriver_path(self):
        """
        :return: String mit Pfad zur chromedriver-Programmdatei
        """
        chromedriver_from_env = os.getenv("VACCIPY_CHROMEDRIVER")
        if chromedriver_from_env:
            return chromedriver_from_env

        # Chromedriver anhand des OS auswählen
        if 'linux' in self.operating_system:
            if "64" in platform.architecture() or sys.maxsize > 2**32:
                return os.path.join(
                    self.PATH, "tools/chromedriver/chromedriver-linux-64")
            else:
                return os.path.join(
                    self.PATH, "tools/chromedriver/chromedriver-linux-32")
        elif 'windows' in self.operating_system:
            return os.path.join(self.PATH,
                                "tools/chromedriver/chromedriver-windows.exe")
        elif 'darwin' in self.operating_system:
            if "arm" in platform.processor().lower():
                return os.path.join(self.PATH,
                                    "tools/chromedriver/chromedriver-mac-m1")
            else:
                return os.path.join(
                    self.PATH, "tools/chromedriver/chromedriver-mac-intel")
        else:
            raise ValueError(
                f"Nicht unterstütztes Betriebssystem {self.operating_system}")

    def get_chromedriver(self, headless):
        chrome_options = Options()

        # deaktiviere Selenium Logging
        chrome_options.add_argument('disable-infobars')
        chrome_options.add_experimental_option('useAutomationExtension', False)
        chrome_options.add_experimental_option("excludeSwitches",
                                               ["enable-automation"])
        chrome_options.add_experimental_option('excludeSwitches',
                                               ['enable-logging'])

        # Zur Behebung von "DevToolsActivePort file doesn't exist"
        #chrome_options.add_argument("-no-sandbox");
        chrome_options.add_argument("-disable-dev-shm-usage")

        # Chrome head is only required for the backup booking process.
        # User-Agent is required for headless, because otherwise the server lets us hang.
        chrome_options.add_argument("user-agent=Mozilla/5.0")

        chromebin_from_env = os.getenv("VACCIPY_CHROME_BIN")
        if chromebin_from_env:
            chrome_options.binary_location = os.getenv("VACCIPY_CHROME_BIN")

        chrome_options.headless = headless

        return Chrome(self.get_chromedriver_path(), options=chrome_options)

    def driver_enter_code(self, driver, plz_impfzentrum):
        """
        TODO xpath code auslagern
        """

        self.log.info("Code eintragen und Mausbewegung / Klicks simulieren. "
                      "Dieser Vorgang kann einige Sekunden dauern.")

        url = f"{self.domain}impftermine/service?plz={plz_impfzentrum}"

        driver.get(url)

        # Queue Bypass
        while True:
            queue_cookie = driver.get_cookie("akavpwr_User_allowed")

            if not queue_cookie \
                    or "Virtueller Warteraum" not in driver.page_source:
                break

            self.log.info("Im Warteraum, Seite neu laden")
            queue_cookie["name"] = "akavpau_User_allowed"
            driver.add_cookie(queue_cookie)

            # Seite neu laden
            time.sleep(5)
            driver.get(url)
            driver.refresh()

        # Klick auf "Auswahl bestätigen" im Cookies-Banner
        button_xpath = "//a[contains(@class,'cookies-info-close')][1]"
        button = WebDriverWait(driver, 1).until(
            EC.element_to_be_clickable((By.XPATH, button_xpath)))
        action = ActionChains(driver)
        action.move_to_element(button).click().perform()

        # Klick auf "Vermittlungscode bereits vorhanden"
        button_xpath = "//input[@name=\"vaccination-approval-checked\"]/.."
        button = WebDriverWait(driver, 1).until(
            EC.element_to_be_clickable((By.XPATH, button_xpath)))
        action = ActionChains(driver)
        action.move_to_element(button).click().perform()

        # Auswahl des ersten Code-Input-Feldes
        input_xpath = "//input[@name=\"ets-input-code-0\"]"
        input_field = WebDriverWait(driver, 1).until(
            EC.element_to_be_clickable((By.XPATH, input_xpath)))
        action = ActionChains(driver)
        action.move_to_element(input_field).click().perform()

        # Code eintragen
        input_field.send_keys(self.code)
        time.sleep(.1)

        # Klick auf "Termin suchen"
        button_xpath = "//app-corona-vaccination-yes//button[@type=\"submit\"]"
        button = WebDriverWait(driver, 1).until(
            EC.element_to_be_clickable((By.XPATH, button_xpath)))
        action = ActionChains(driver)
        action.move_to_element(button).click().perform()

        # Maus-Bewegung hinzufügen (nicht sichtbar)
        for i in range(3):
            try:
                action.move_by_offset(randint(1, 100), randint(1,
                                                               100)).perform()
                time.sleep(randint(1, 3))
            except:
                pass

    def driver_renew_cookies(self, driver, plz_impfzentrum):
        self.driver_enter_code(driver, plz_impfzentrum)

        # prüfen, ob Cookies gesetzt wurden und in Session übernehmen
        try:
            cookie = driver.get_cookie("bm_sz")
            if cookie:
                self.s.cookies.clear()
                self.s.cookies.update(
                    {c['name']: c['value']
                     for c in driver.get_cookies()})
                self.log.info("Browser-Cookie generiert: *{}".format(
                    cookie.get("value")[-6:]))
                return True
            else:
                self.log.error("Cookies können nicht erstellt werden!")
                return False
        except:
            return False

    def driver_renew_cookies_code(self, driver, plz_impfzentrum, manual=False):
        self.driver_enter_code(driver, plz_impfzentrum)
        if manual:
            self.log.warn(
                "Du hast jetzt 30 Sekunden Zeit möglichst viele Elemente im Chrome Fenster anzuklicken. Das Fenster schließt sich automatisch."
            )
            time.sleep(30)
        # prüfen, ob Cookies gesetzt wurden und in Session übernehmen
        try:
            cookie = driver.get_cookie("bm_sz").get("value")
            akavpau = driver.get_cookie("akavpau_User_allowed").get("value")
            if cookie:
                self.s.cookies.clear()
                self.s.cookies.update({
                    "bm_sz": cookie,
                    "akavpau_User_allowed": akavpau
                })
                self.log.info("Browser-Cookie generiert: *{}".format(
                    cookie.get("value")[-6:]))
                return True
            else:
                self.log.error("Cookies können nicht erstellt werden!")
                return False
        except:
            return False

    def driver_book_appointment(self, driver, plz_impfzentrum):
        timestamp = time.strftime("%Y%m%d-%H%M%S")
        filepath = os.path.join(self.PATH, "tools\\log\\")
        url = f"{self.domain}impftermine/service?plz={plz_impfzentrum}"

        self.driver_enter_code(driver, plz_impfzentrum)

        try:
            # Klick auf "Termin suchen"
            button_xpath = "//button[@data-target=\"#itsSearchAppointmentsModal\"]"
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
        except:
            self.log.error("Termine können nicht gesucht werden")
            try:
                driver.save_screenshot(filepath + "errorterminsuche" +
                                       timestamp + ".png")
            except:
                self.log.error("Screenshot konnte nicht gespeichert werden")
            pass

        # Termin auswählen
        try:
            time.sleep(3)
            button_xpath = '//*[@id="itsSearchAppointmentsModal"]/div/div/div[2]/div/div/form/div[1]/div[2]/label/div[2]/div'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.5)
        except:
            self.log.error("Termine können nicht ausgewählt werden")
            try:
                with open(filepath + "errorterminauswahl" + timestamp +
                          ".html",
                          'w',
                          encoding='utf-8') as file:
                    file.write(str(driver.page_source))
                driver.save_screenshot(filepath + "errorterminauswahl" +
                                       timestamp + ".png")
            except:
                self.log.error(
                    "HTML und Screenshot konnten nicht gespeichert werden")
            pass

        # Klick Button "AUSWÄHLEN"
        try:
            button_xpath = '//*[@id="itsSearchAppointmentsModal"]//button[@type="submit"]'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.5)
        except:
            self.log.error("Termine können nicht ausgewählt werden (Button)")
            pass

        # Klick Daten erfassen
        try:
            button_xpath = '/html/body/app-root/div/app-page-its-search/div/div/div[2]/div/div/div[5]/div/div[2]/div[2]/div[2]/button'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.5)
        except:
            self.log.error("1. Daten können nicht erfasst werden")
            pass
        try:
            # Klick Anrede
            arrAnreden = ["Herr", "Frau", "Kind", "Divers"]
            if self.kontakt['anrede'] in arrAnreden:
                button_xpath = '//*[@id="itsSearchContactModal"]//app-booking-contact-form//div[contains(@class,"ets-radio-wrapper")]/label[@class="ets-radio-control"]/span[contains(text(),"' + self.kontakt[
                    'anrede'] + '")]'
            else:
                button_xpath = '//*[@id="itsSearchContactModal"]//app-booking-contact-form//div[contains(@class,"ets-radio-wrapper")]/label[@class="ets-radio-control"]/span[contains(text(),"Divers")]'

            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()

            # Input Vorname
            input_xpath = '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="firstname"]'
            input_field = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, input_xpath)))
            action.move_to_element(input_field).click().perform()
            input_field.send_keys(self.kontakt['vorname'])

            # Input Nachname
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="lastname"]'
            )
            input_field.send_keys(self.kontakt['nachname'])

            # Input PLZ
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="zip"]'
            )
            input_field.send_keys(self.kontakt['plz'])

            # Input City
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="city"]'
            )
            input_field.send_keys(self.kontakt['ort'])

            # Input Strasse
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="street"]'
            )
            input_field.send_keys(self.kontakt['strasse'])

            # Input Hasunummer
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="housenumber"]'
            )
            input_field.send_keys(self.kontakt['hausnummer'])

            # Input Telefonnummer
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="phone"]'
            )
            input_field.send_keys(self.kontakt['phone'].replace("+49", ""))

            # Input Mail
            input_field = driver.find_element_by_xpath(
                '//*[@id="itsSearchContactModal"]//app-booking-contact-form//input[@formcontrolname="notificationReceiver"]'
            )
            input_field.send_keys(self.kontakt['notificationReceiver'])
        except:
            self.log.error("Kontaktdaten können nicht eingegeben werden")
            try:
                driver.save_screenshot(filepath + "errordateneingeben" +
                                       timestamp + ".png")
            except:
                self.log.error("Screenshot konnte nicht gespeichert werden")
            pass

        # Klick Button "ÜBERNEHMEN"
        try:
            button_xpath = '//*[@id="itsSearchContactModal"]//button[@type="submit"]'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
            time.sleep(.7)
        except:
            self.log.error("Button ÜBERNEHMEN kann nicht gedrückt werden")
            pass

        # Termin buchen
        try:
            button_xpath = '/html/body/app-root/div/app-page-its-search/div/div/div[2]/div/div/div[5]/div/div[3]/div[2]/div[2]/button'
            button = WebDriverWait(driver, 1).until(
                EC.element_to_be_clickable((By.XPATH, button_xpath)))
            action = ActionChains(driver)
            action.move_to_element(button).click().perform()
        except:
            self.log.error("Button Termin buchen kann nicht gedrückt werden")
            pass
        time.sleep(3)
        if "Ihr Termin am" in str(driver.page_source):
            msg = "Termin erfolgreich gebucht!"
            self.log.success(msg)
            desktop_notification(operating_system=self.operating_system,
                                 title="Terminbuchung:",
                                 message=msg)
            return True
        else:
            self.log.error(
                "Automatisierte Terminbuchung fehlgeschlagen. Termin manuell im Fenster oder im Browser buchen."
            )
            print(
                f"Link für manuelle Buchung im Browser: {self.domain}impftermine/suche/{self.code}/{plz_impfzentrum}"
            )
            time.sleep(10 * 60)
            return False

    @retry_on_failure()
    def renew_cookies(self):
        """
        Cookies der Session erneuern, wenn sie abgelaufen sind.
        :return:
        """

        self.log.info("Browser-Cookies generieren")
        driver = self.get_chromedriver(headless=True)
        try:
            return self.driver_renew_cookies(driver,
                                             choice(self.plz_impfzentren))
        finally:
            driver.quit()

    @retry_on_failure()
    def renew_cookies_code(self, manual=False):
        """
        Cookies der Session erneuern, wenn sie abgelaufen sind.
        :return:
        """

        self.log.info("Browser-Cookies generieren")
        driver = self.get_chromedriver(headless=False)
        try:
            return self.driver_renew_cookies_code(driver,
                                                  choice(self.plz_impfzentren),
                                                  manual)
        finally:
            driver.quit()

    @retry_on_failure()
    def book_appointment(self):
        """
        Backup Prozess:
        Wenn die Terminbuchung mit dem Bot nicht klappt, wird das
        Browserfenster geöffnet und die Buchung im Browser beendet
        :return:
        """

        self.log.info("Termin über Selenium buchen")
        driver = self.get_chromedriver(headless=False)
        try:
            return self.driver_book_appointment(driver, self.plz_termin)
        finally:
            driver.quit()

    @retry_on_failure()
    def login(self):
        """Einloggen mittels Code, um qualifizierte Impfstoffe zu erhalten.
        Dieser Schritt ist wahrscheinlich nicht zwingend notwendig, aber schadet auch nicht.

        :return: bool
        """

        path = f"rest/login?plz={choice(self.plz_impfzentren)}"

        res = self.s.get(self.domain + path, timeout=15)
        if res.ok:
            # Checken, welche Impfstoffe für das Alter zur Verfügung stehen
            self.qualifikationen = res.json().get("qualifikationen")

            if self.qualifikationen:
                zugewiesene_impfstoffe = set()

                for q in self.qualifikationen:
                    for verfuegbare_q in self.verfuegbare_qualifikationen:
                        if verfuegbare_q["qualification"] == q:
                            zugewiesene_impfstoffe.update(
                                verfuegbare_q["impfstoffe"])

                self.log.info("Erfolgreich mit Code eingeloggt")
                self.log.info(
                    f"Mögliche Impfstoffe: {list(zugewiesene_impfstoffe)}")
                print(" ")

                return True
            else:
                self.log.warn("Keine qualifizierten Impfstoffe verfügbar")
        else:
            return False

    @retry_on_failure()
    def termin_suchen(self, plz: str, zeitrahmen: dict):
        """Es wird nach einen verfügbaren Termin in der gewünschten PLZ gesucht.
        Ausgewählt wird der erstbeste Termin, welcher im entsprechenden Zeitraum liegt (!).
        Zurückgegeben wird das Ergebnis der Abfrage und der Status-Code.
        Bei Status-Code > 400 müssen die Cookies erneuert werden.

        Beispiel für ein Termin-Paar:

        [{
            'slotId': 'slot-56817da7-3f46-4f97-9868-30a6ddabcdef',
            'begin': 1616999901000,
            'bsnr': '005221080'
        }, {
            'slotId': 'slot-d29f5c22-384c-4928-922a-30a6ddabcdef',
            'begin': 1623999901000,
            'bsnr': '005221080'
        }]

        :return: bool, status-code
        """

        path = f"rest/suche/impfterminsuche?plz={plz}"

        while True:
            res = self.s.get(self.domain + path, timeout=15)
            if not res.ok or 'Virtueller Warteraum des Impfterminservice' not in res.text:
                break
            self.log.info('Warteraum... zZz...')
            time.sleep(30)

        if res.ok:
            res_json = res.json()
            terminpaare = res_json.get("termine")
            self.termin_anzahl = len(terminpaare)
            if terminpaare:
                terminpaare_angenommen = [
                    tp for tp in terminpaare
                    if terminpaar_im_zeitrahmen(tp, zeitrahmen)
                ]
                terminpaare_abgelehnt = [
                    tp for tp in terminpaare
                    if tp not in terminpaare_angenommen
                ]
                impfzentrum = self.verfuegbare_impfzentren.get(plz)
                zentrumsname = impfzentrum.get('Zentrumsname').strip()
                ort = impfzentrum.get('Ort')
                for tp_abgelehnt in terminpaare_abgelehnt:
                    self.log.warn(
                        "Termin gefunden - jedoch nicht im entsprechenden Zeitraum:"
                    )
                    self.log.info('-' * 50)
                    self.log.warn(f"'{zentrumsname}' in {plz} {ort}")
                    for num, termin in enumerate(tp_abgelehnt, 1):
                        ts = datetime.fromtimestamp(
                            termin["begin"] /
                            1000).strftime('%d.%m.%Y um %H:%M Uhr')
                        self.log.warn(f"{num}. Termin: {ts}")
                    self.log.info('-' * 50)
                if terminpaare_angenommen:
                    # Auswahl des erstbesten Terminpaares
                    self.terminpaar = choice(terminpaare_angenommen)
                    self.plz_termin = plz
                    self.log.success(f"Termin gefunden!")
                    self.log.success(f"'{zentrumsname}' in {plz} {ort}")
                    for num, termin in enumerate(self.terminpaar, 1):
                        ts = datetime.fromtimestamp(
                            termin["begin"] /
                            1000).strftime('%d.%m.%Y um %H:%M Uhr')
                        self.log.success(f"{num}. Termin: {ts}")
                    if ENABLE_BEEPY:
                        beepy.beep('coin')
                    else:
                        print("\a")
                    return True, 200
            else:
                self.log.info(f"Keine Termine verfügbar in {plz}")
        elif res.status_code == 401:
            self.log.error(
                f"Terminpaare können nicht geladen werden: Impf-Code kann nicht für "
                f"die PLZ '{plz}' verwendet werden.")
            quit()
        else:
            self.log.error(
                f"Terminpaare können nicht geladen werden: {res.text}")
        return False, res.status_code

    @retry_on_failure()
    def termin_buchen(self):
        """Termin wird gebucht für die Kontaktdaten, die beim Starten des
        Programms eingetragen oder aus der JSON-Datei importiert wurden.

        :return: bool
        """

        path = "rest/buchung"

        # Daten für Impftermin sammeln
        data = {
            "plz": self.plz_termin,
            "slots": [termin.get("slotId") for termin in self.terminpaar],
            "qualifikationen": self.qualifikationen,
            "contact": self.kontakt
        }

        res = self.s.post(self.domain + path, json=data, timeout=15)

        if res.status_code == 201:
            msg = "Termin erfolgreich gebucht!"
            self.log.success(msg)
            desktop_notification(operating_system=self.operating_system,
                                 title="Terminbuchung:",
                                 message=msg)
            return True

        elif res.status_code == 429:
            msg = "Anfrage wurde von der Botprotection geblockt. Cookies werden erneuert und die Buchung wiederholt."
            self.log.error(msg)
            self.renew_cookies_code()
            res = self.s.post(self.domain + path, json=data, timeout=15)
            if res.status_code == 201:
                msg = "Termin erfolgreich gebucht!"
                self.log.success(msg)
                desktop_notification(operating_system=self.operating_system,
                                     title="Terminbuchung:",
                                     message=msg)
                return True
            else:
                # Termin über Selenium Buchen
                return self.book_appointment()

        elif res.status_code >= 400:
            data = res.json()
            try:
                error = data['errors']['status']
            except KeyError:
                error = ''
            if 'nicht mehr verfügbar' in error:
                msg = f"Diesen Termin gibts nicht mehr: {error}"
                #Bei Terminanzahl = 1 11 Minuten warten und danach fortsetzen.
                if self.termin_anzahl == 1:
                    msg = f"Diesen Termin gibts nicht mehr: {error}. Die Suche wird in 11 Minuten fortgesetzt"
                    self.log.error(msg)
                    time.sleep(11 * 60)
                    return False
            else:
                msg = f"Termin konnte nicht gebucht werden: {data}"
        else:
            msg = f"Unbekannter Statuscode: {res.status_code}"

        self.log.error(msg)
        desktop_notification(operating_system=self.operating_system,
                             title="Terminbuchung:",
                             message=msg)
        return False

    @retry_on_failure()
    def code_anfordern(self, mail, telefonnummer, plz_impfzentrum,
                       geburtsdatum):
        """
        SMS-Code beim Impfterminservice anfordern.

        :param mail: Mail für Empfang des Codes
        :param telefonnummer: Telefonnummer für SMS-Code, inkl. Präfix +49
        :param plz_impfzentrum: PLZ des Impfzentrums, für das ein Code erstellt werden soll
        :param geburtsdatum: Geburtsdatum der Person
        :return:
        """

        path = "rest/smspin/anforderung"

        data = {
            "plz":
            plz_impfzentrum,
            "email":
            mail,
            "phone":
            telefonnummer,
            "birthday":
            "{}-{:02d}-{:02d}".format(
                *reversed([int(d) for d in geburtsdatum.split(".")])),
            "einzeltermin":
            False
        }

        while True:
            res = self.s.post(self.domain + path, json=data, timeout=15)
            if res.ok:
                token = res.json().get("token")
                return token
            elif res.status_code == 429:
                self.log.error(
                    "Anfrage wurde von der Botprotection geblockt.\n"
                    "Die Cookies müssen manuell im Browser generiert werden.\n"
                )
                self.renew_cookies_code(True)
            else:
                self.log.error(f"Code kann nicht angefragt werden: {res.text}")
                return None

    @retry_on_failure()
    def code_bestaetigen(self, token, sms_pin):
        """
        Bestätigung der Code-Generierung mittels SMS-Code

        :param token: Token der Code-Erstellung
        :param sms_pin: 6-stelliger SMS-Code
        :return:
        """

        path = f"rest/smspin/verifikation"
        data = {"token": token, "smspin": sms_pin}
        while True:
            res = self.s.post(self.domain + path, json=data, timeout=15)
            if res.ok:
                self.log.success(
                    "Der Impf-Code wurde erfolgreich angefragt, bitte prüfe deine Mails!"
                )
                return True
            elif res.status_code == 429:
                self.log.error("Cookies müssen erneuert werden.")
                self.renew_cookies_code()
            else:
                self.log.error(f"Code-Verifikation fehlgeschlagen: {res.text}")
                return False

    @staticmethod
    def terminsuche(code: str,
                    plz_impfzentren: list,
                    kontakt: dict,
                    PATH: str,
                    zeitrahmen: dict = dict(),
                    check_delay: int = 30):
        """
        Workflow für die Terminbuchung.

        :param code: 14-stelliger Impf-Code
        :param plz_impfzentren: Liste mit PLZ von Impfzentren
        :param kontakt: Kontaktdaten der zu impfenden Person als JSON
        :param check_delay: Zeit zwischen Iterationen der Terminsuche
        :return:
        """

        validate_kontakt(kontakt)
        validate_zeitrahmen(zeitrahmen)

        its = ImpfterminService(code, plz_impfzentren, kontakt, PATH)
        its.renew_cookies()

        # login ist nicht zwingend erforderlich
        its.login()

        while True:
            termin_gefunden = False
            while not termin_gefunden:

                # durchlaufe jede eingegebene PLZ und suche nach Termin
                for plz in its.plz_impfzentren:
                    termin_gefunden, status_code = its.termin_suchen(
                        plz, zeitrahmen)

                    # Durchlauf aller PLZ unterbrechen, wenn Termin gefunden wurde
                    if termin_gefunden:
                        break
                    # Cookies erneuern
                    elif status_code >= 400:
                        its.renew_cookies()
                    # Suche pausieren
                    if not termin_gefunden:
                        time.sleep(check_delay)

            # Programm beenden, wenn Termin gefunden wurde
            if its.termin_buchen():
                return True