├── requirements.txt ├── LICENSE.md ├── example_config.yml ├── README.md └── medisnip.py /requirements.txt: -------------------------------------------------------------------------------- 1 | zeep 2 | pyyaml 3 | coloredlogs 4 | python-pushover -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Marek Wajdzik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /example_config.yml: -------------------------------------------------------------------------------- 1 | medicover: 2 | card_id: 1234567 # Medicover MRN (your card number) 3 | password: change.me # Medicover account password 4 | medisnip: 5 | doctor_locator_id: 123*1234*12345*123456 # Unique identificator pattern (region_id*specialty_id*clinic_id*doctor_id) of specialist. Can be obtained by running ./medisnip.py -l - beware - listing is slow 6 | # or by link https://mol.medicover.pl/MyVisits?regionId=205&bookingTypeId=2&specializationId=192&serviceId=&clinicId=-1&languageId=-1&doctorId=-1 7 | # -1 means (Any) 8 | lookup_time_days: 25 # How many days from now should script look at. 9 | pushover: 10 | user_key: PUSHOVER_USER_KEY # Your pushover.net user key 11 | api_token: API_TOKEN # pushover.net App API Token 12 | message_template: "New visit! {AppointmentDate} at {ClinicPublicName} - {DoctorName} ({SpecialtyName})" # Message template, available fields: SpecialtyId ClinicId VendorTypeId ClinicName EndTime HiddenSlot ErrorText ErrorCode DateStartTime VendorTypeCd OARule ClinicPublicName SpecialtyName SysVisitTypeId DoctorLanguages LinkedReferralId DoctorName ConsultationRoomId Position DebugData DoctorScheduleId AppointmentDate ScheduleDate DoctorId ServiceId StartTime Duration 13 | title: "New visit available!" # Pushover message topic 14 | misc: 15 | notifydb: ./surgeon_data.db # State file used to remember which notifications has been sent already 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MediSnip - Medicover Appointment Sniper 2 | ======================================= 3 | Simple tool to notify about available slot in Medicover medical care service using pushover notifications. 4 | It uses SOAP service which was discovered in sources of Android mobile application. 5 | 6 | How to use MediSnip? 7 | -------------------- 8 | First of all create virtualenv and install python requirements from requirements.txt 9 | 10 | 1) For each specialist create configuration file (yaml format) and save it for example as my_favourite_surgeon.yml: 11 | ``` 12 | medicover: 13 | card_id: 1234567 # Medicover MRN (your card number) 14 | password: change.me # Medicover account password 15 | medisnip: 16 | doctor_locator_id: 123*1234*12345*123456 # Unique identificator pattern (region_id*specialty_id*clinic_id*doctor_id) of specialist. Can be obtained by running ./medisnip.py -l - beware - listing is slow 17 | # or by link https://mol.medicover.pl/MyVisits?regionId=205&bookingTypeId=2&specializationId=192&serviceId=&clinicId=-1&languageId=-1&doctorId=-1 18 | # -1 means (Any) 19 | lookup_time_days: 25 # How many days from now should script look at. 20 | pushover: 21 | user_key: PUSHOVER_USER_KEY # Your pushover.net user key 22 | api_token: API_TOKEN # pushover.net App API Token 23 | message_template: "New visit! {AppointmentDate} at {ClinicPublicName} - {DoctorName} ({SpecialtyName})" # Message template, available fields: SpecialtyId ClinicId VendorTypeId ClinicName EndTime HiddenSlot ErrorText ErrorCode DateStartTime VendorTypeCd OARule ClinicPublicName SpecialtyName SysVisitTypeId DoctorLanguages LinkedReferralId DoctorName ConsultationRoomId Position DebugData DoctorScheduleId AppointmentDate ScheduleDate DoctorId ServiceId StartTime Duration 24 | title: "New visit available!" # Pushover message topic 25 | misc: 26 | notifydb: ./surgeon_data.db # State file used to remember which notifications has been sent already 27 | ``` 28 | 29 | 2) Add script to crontab: 30 | ``` 31 | */3 * * * * /path/to/medisnip/venv/bin/python /path/to/medisnip.py -c /path/to/my_favourite_surgeon.yml 32 | ``` 33 | (Additionaly you can tune logger settings in script) 34 | 35 | 3) Wait for new appointment notifications in your pushover app on mobile :)! 36 | 37 | License 38 | ------- 39 | The MIT License (MIT) 40 | 41 | Copyright (c) 2018 Marek Wajdzik 42 | 43 | Permission is hereby granted, free of charge, to any person obtaining a copy 44 | of this software and associated documentation files (the "Software"), to deal 45 | in the Software without restriction, including without limitation the rights 46 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 47 | copies of the Software, and to permit persons to whom the Software is 48 | furnished to do so, subject to the following conditions: 49 | 50 | The above copyright notice and this permission notice shall be included in 51 | all copies or substantial portions of the Software. 52 | 53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 54 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 55 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 56 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 57 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 59 | THE SOFTWARE. 60 | 61 | Contact 62 | ------- 63 | Marek Wajdzik 64 | -------------------------------------------------------------------------------- /medisnip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import zeep 4 | import os 5 | import argparse 6 | import yaml 7 | import coloredlogs 8 | import logging 9 | import uuid 10 | import json 11 | import datetime 12 | import pushover 13 | import shelve 14 | 15 | #Setup logging 16 | coloredlogs.install(level="INFO") 17 | log = logging.getLogger("main") 18 | 19 | class MediSnip(object): 20 | MEDICOVER_API = "https://api.medicover.pl/MOB/MOB.WebServices/Service.svc/basic?singleWsdl" 21 | def __init__(self, configuration_file="medisnip.yaml"): 22 | #Setup logger 23 | self.log = logging.getLogger("medisnip") 24 | self.log.info("MediSnip logger initialized") 25 | #Open configuration file 26 | try: 27 | config_data = open( 28 | os.path.expanduser( 29 | configuration_file 30 | ), 31 | 'r' 32 | ).read() 33 | except IOError: 34 | raise Exception('Cannot open configuration file ({file})!'.format(file=configuration_file)) 35 | #Try to parse yaml configuration 36 | try: 37 | self.config = yaml.load(config_data) 38 | except Exception as yaml_error: 39 | raise Exception('Configuration problem: {error}'.format(error=yaml_error)) 40 | transport = zeep.Transport() 41 | transport.session.headers[ 42 | 'User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36' 43 | self.medicover = zeep.Client(self.MEDICOVER_API, transport=transport) 44 | self.mol = self.medicover.service 45 | self.log.info("MediSnip client initialized") 46 | #Try to login to MOL Service 47 | ticket = json.loads(self.mol.MobileLogin_StrongTypeInput( 48 | self.config['medicover']['card_id'], 49 | self.config['medicover']['password'], 50 | 'NewMOB', 51 | uuid.uuid4, 52 | 'Android', 53 | '7.1' 54 | )) 55 | if ticket['TicketId'] is None: 56 | raise Exception("Login or password is incorrect") 57 | else: 58 | self.ticket = ticket['TicketId'] 59 | self.person = ticket['Id'] 60 | self.log.info("Succesfully logged in! (TicketID: {ticket}, PersonID: {person})".format( 61 | ticket=self.ticket, 62 | person=self.person 63 | )) 64 | 65 | def list_codes(self): 66 | # Region -> Specialty -> Clinic -> Doctor 67 | self.log.info('Listing available ids!') 68 | #Region 69 | for region in self.mol.LoadRegions( 70 | ticketId=self.ticket, 71 | input={} 72 | ): 73 | #Specialty (cached) 74 | specialties = self.mol.LoadSpecialties_Cached( 75 | ticketId=self.ticket, 76 | input={ 77 | 'RegionId': region['RegionId'] 78 | } 79 | ) 80 | if specialties: 81 | for specialty in specialties: 82 | #Clinics (cached) 83 | clinics = self.mol.LoadClinics_Cached( 84 | ticketId=self.ticket, 85 | input={ 86 | 'SpecialtyId': specialty['SpecialtyId'], 87 | 'RegionId': region['RegionId'] 88 | } 89 | ) 90 | if clinics: 91 | for clinic in clinics: 92 | #Load doctors (cached): 93 | doctors = self.mol.LoadDoctors_Cached( 94 | ticketId=self.ticket, 95 | input={ 96 | 'SpecialtyId': specialty['SpecialtyId'], 97 | 'RegionId': region['RegionId'], 98 | 'ClinicId': clinic['ClinicId'] 99 | } 100 | ) 101 | if doctors: 102 | for doctor in doctors: 103 | self.log.info(u"DoctorLocatorID: {id} for: Location: {RegionPublicName} {ClinicPublicName} Specialty: {SpecialtyName} Doctor: {DoctorName}".format( 104 | id="{}-{}-{}-{}".format( 105 | region['RegionId'], 106 | specialty['SpecialtyId'], 107 | clinic['ClinicId'], 108 | doctor['DoctorId'] 109 | ), 110 | RegionPublicName=region['RegionPublicName'], 111 | ClinicPublicName=clinic['ClinicPublicName'], 112 | SpecialtyName=specialty['SpecialtyName'], 113 | DoctorName=doctor['DoctorName'] 114 | )) 115 | def check_slots(self): 116 | try: 117 | (region_id, specialty_id, clinic_id, doctor_id) = self.config['medisnip']['doctor_locator_id'].strip().split('*') 118 | except ValueError: 119 | raise Exception('DoctorLocatorID seems to be in invalid format') 120 | self.log.info("Searching for appointments.") 121 | appointments = self.mol.GetFreeSlots( 122 | ticketId=self.ticket, 123 | input={ 124 | 'RegionId': region_id, 125 | 'SpecialtyId': specialty_id, 126 | 'ClinicId': clinic_id, 127 | 'DoctorId': doctor_id, 128 | 'StartDate': datetime.datetime.now(), 129 | 'EndDate': datetime.datetime.now()+datetime.timedelta( 130 | days=self.config['medisnip']['lookup_time_days'] 131 | ) 132 | } 133 | ) 134 | if appointments: 135 | for appointment in appointments: 136 | ap_dict = zeep.helpers.serialize_object( 137 | appointment, 138 | target_cls=dict 139 | ) 140 | self.log.info("Appointment found ({data})".format( 141 | data=str(ap_dict) 142 | )) 143 | self._notify(ap_dict, self.config['pushover']['message_template'].format( 144 | **ap_dict 145 | ) 146 | ) 147 | else: 148 | self.log.info("No appointments found.") 149 | 150 | def _notify(self, data, message): 151 | state = shelve.open(self.config['misc']['notifydb']) 152 | notifications = state.get(str(data['DoctorId']), []) 153 | if not data['AppointmentDate'] in notifications: 154 | notifications.append(data['AppointmentDate']) 155 | self.log.info(u'Sending notification: {}'.format(message)) 156 | pushover.init(self.config['pushover']['api_token']) 157 | pushover.Client(self.config['pushover']['user_key']).send_message(message, title=self.config['pushover']['title']) 158 | else: 159 | self.log.info('Notification was already sent.') 160 | state[str(data['DoctorId'])] = notifications 161 | state.close() 162 | 163 | if __name__=="__main__": 164 | log.info("MediSnip - Medicover Appointment Sniper ") 165 | parser = argparse.ArgumentParser() 166 | parser.add_argument( 167 | "-c", "--config", 168 | help="Configuration file path", default="~/.medisnip.yml" 169 | ) 170 | parser.add_argument( 171 | "-l", "--list", 172 | help="List all DoctorLocatorID's (needed for configuration)", action='store_true') 173 | args = parser.parse_args() 174 | medisnip = MediSnip(configuration_file=args.config) 175 | if args.list: 176 | medisnip.list_codes() 177 | else: 178 | medisnip.check_slots() 179 | --------------------------------------------------------------------------------