|
| 1 | +import requests |
| 2 | +import logging |
| 3 | + |
| 4 | + |
| 5 | +class MyQAPI: |
| 6 | + """Class for interacting with the MyQ iOS App API.""" |
| 7 | + |
| 8 | + LIFTMASTER = 'liftmaster' |
| 9 | + CHAMBERLAIN = 'chamberlain' |
| 10 | + CRAFTMASTER = 'craftmaster' |
| 11 | + |
| 12 | + SUPPORTED_BRANDS = [LIFTMASTER, CHAMBERLAIN, CRAFTMASTER] |
| 13 | + SUPPORTED_DEVICE_TYPE_NAMES = ['GarageDoorOpener', 'Garage Door Opener WGDO', 'VGDO'] |
| 14 | + |
| 15 | + APP_ID = 'app_id' |
| 16 | + HOST_URI = 'host_uri' |
| 17 | + |
| 18 | + BRAND_MAPPINGS = { |
| 19 | + LIFTMASTER: { |
| 20 | + APP_ID: 'JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu', |
| 21 | + HOST_URI: 'myqexternal.myqdevice.com' |
| 22 | + }, |
| 23 | + CHAMBERLAIN: { |
| 24 | + APP_ID: 'Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB%2Fi', |
| 25 | + HOST_URI: 'myqexternal.myqdevice.com' |
| 26 | + }, |
| 27 | + CRAFTMASTER: { |
| 28 | + APP_ID: 'eU97d99kMG4t3STJZO/Mu2wt69yTQwM0WXZA5oZ74/ascQ2xQrLD/yjeVhEQccBZ', |
| 29 | + HOST_URI: 'craftexternal.myqdevice.com' |
| 30 | + } |
| 31 | + } |
| 32 | + |
| 33 | + STATE_OPEN = 'open' |
| 34 | + STATE_CLOSED = 'closed' |
| 35 | + |
| 36 | + LOCALE = "en" |
| 37 | + LOGIN_ENDPOINT = "api/user/validatewithculture" |
| 38 | + DEVICE_LIST_ENDPOINT = "api/v4/userdevicedetails/get" |
| 39 | + DEVICE_SET_ENDPOINT = "api/v4/DeviceAttribute/PutDeviceAttribute" |
| 40 | + HEADERS = {'User-Agent': 'Chamberlain/3773 (iPhone; iOS 10.0.1; Scale/2.00)'} |
| 41 | + |
| 42 | + REQUEST_TIMEOUT = 3.0 |
| 43 | + |
| 44 | + DOOR_STATE = { |
| 45 | + '1': STATE_OPEN, #'open', |
| 46 | + '2': STATE_CLOSED, #'close', |
| 47 | + '4': STATE_OPEN, #'opening', |
| 48 | + '5': STATE_CLOSED, #'closing', |
| 49 | + '8': STATE_OPEN, #'in_transition', |
| 50 | + '9': STATE_OPEN, #'open' |
| 51 | + } |
| 52 | + |
| 53 | + logger = logging.getLogger(__name__) |
| 54 | + |
| 55 | + def __init__(self, username, password, brand): |
| 56 | + """Initialize the API object.""" |
| 57 | + self.username = username |
| 58 | + self.password = password |
| 59 | + self.brand = brand |
| 60 | + self.security_token = None |
| 61 | + self._logged_in = False |
| 62 | + self._valid_brand = False |
| 63 | + |
| 64 | + def is_supported_brand(self): |
| 65 | + try: |
| 66 | + brand = self.BRAND_MAPPINGS[self.brand]; |
| 67 | + except KeyError: |
| 68 | + return False |
| 69 | + |
| 70 | + return True |
| 71 | + |
| 72 | + def is_login_valid(self): |
| 73 | + """Log in to the MyQ service.""" |
| 74 | + params = { |
| 75 | + 'username': self.username, |
| 76 | + 'password': self.password, |
| 77 | + 'appId': self.BRAND_MAPPINGS[self.brand][self.APP_ID], |
| 78 | + 'culture': self.LOCALE |
| 79 | + } |
| 80 | + |
| 81 | + try: |
| 82 | + login = requests.get( |
| 83 | + 'https://{host_uri}/{login_endpoint}'.format( |
| 84 | + host_uri=self.BRAND_MAPPINGS[self.brand][self.HOST_URI], |
| 85 | + login_endpoint=self.LOGIN_ENDPOINT), |
| 86 | + params=params, |
| 87 | + headers=self.HEADERS, |
| 88 | + timeout=self.REQUEST_TIMEOUT |
| 89 | + ) |
| 90 | + |
| 91 | + login.raise_for_status() |
| 92 | + except requests.exceptions.HTTPError as err: |
| 93 | + self.logger.error("MyQ - API Error %s", ex) |
| 94 | + return False |
| 95 | + |
| 96 | + try: |
| 97 | + self.security_token = login.json()['SecurityToken'] |
| 98 | + except KeyError: |
| 99 | + return False |
| 100 | + |
| 101 | + return True |
| 102 | + |
| 103 | + def get_devices(self): |
| 104 | + """List all MyQ devices.""" |
| 105 | + if not self._logged_in: |
| 106 | + self._logged_in = self.is_login_valid() |
| 107 | + |
| 108 | + params = { |
| 109 | + 'appId': self.BRAND_MAPPINGS[self.brand][self.APP_ID], |
| 110 | + 'securityToken': self.security_token |
| 111 | + } |
| 112 | + |
| 113 | + try: |
| 114 | + devices = requests.get( |
| 115 | + 'https://{host_uri}/{device_list_endpoint}'.format( |
| 116 | + host_uri=self.BRAND_MAPPINGS[self.brand][self.HOST_URI], |
| 117 | + device_list_endpoint=self.DEVICE_LIST_ENDPOINT), |
| 118 | + params=params, |
| 119 | + headers=self.HEADERS |
| 120 | + ) |
| 121 | + |
| 122 | + devices.raise_for_status() |
| 123 | + |
| 124 | + devices = devices.json()['Devices'] |
| 125 | + |
| 126 | + return devices |
| 127 | + except requests.exceptions.HTTPError as err: |
| 128 | + self.logger.error("MyQ - API Error %s", ex) |
| 129 | + return False |
| 130 | + |
| 131 | + def get_garage_doors(self): |
| 132 | + """List only MyQ garage door devices.""" |
| 133 | + devices = self.get_devices() |
| 134 | + |
| 135 | + garage_doors = [] |
| 136 | + |
| 137 | + try: |
| 138 | + for device in devices: |
| 139 | + if device['MyQDeviceTypeName'] in self.SUPPORTED_DEVICE_TYPE_NAMES: |
| 140 | + dev = {} |
| 141 | + for attribute in device['Attributes']: |
| 142 | + if attribute['AttributeDisplayName'] == 'desc': |
| 143 | + dev['deviceid'] = device['MyQDeviceId'] |
| 144 | + dev['name'] = attribute['Value'] |
| 145 | + garage_doors.append(dev) |
| 146 | + |
| 147 | + return garage_doors |
| 148 | + except TypeError: |
| 149 | + return False |
| 150 | + |
| 151 | + def get_status(self, device_id): |
| 152 | + """List only MyQ garage door devices.""" |
| 153 | + devices = self.get_devices() |
| 154 | + |
| 155 | + for device in devices: |
| 156 | + if device['MyQDeviceTypeName'] in self.SUPPORTED_DEVICE_TYPE_NAMES and device['MyQDeviceId'] == device_id: |
| 157 | + dev = {} |
| 158 | + for attribute in device['Attributes']: |
| 159 | + if attribute['AttributeDisplayName'] == 'doorstate': |
| 160 | + garage_state = attribute['Value'] |
| 161 | + |
| 162 | + garage_state = self.DOOR_STATE[garage_state] |
| 163 | + return garage_state |
| 164 | + |
| 165 | + def close_device(self, device_id): |
| 166 | + """Close MyQ Device.""" |
| 167 | + return self.set_state(device_id, '0') |
| 168 | + |
| 169 | + def open_device(self, device_id): |
| 170 | + """Open MyQ Device.""" |
| 171 | + return self.set_state(device_id, '1') |
| 172 | + |
| 173 | + def set_state(self, device_id, state): |
| 174 | + """Set device state.""" |
| 175 | + payload = { |
| 176 | + 'AttributeName': 'desireddoorstate', |
| 177 | + 'MyQDeviceId': device_id, |
| 178 | + 'ApplicationId': 'JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu', |
| 179 | + 'AttributeValue': state, |
| 180 | + 'SecurityToken': self.security_token, |
| 181 | + } |
| 182 | + |
| 183 | + try: |
| 184 | + device_action = requests.put( |
| 185 | + 'https://{host_uri}/{device_set_endpoint}'.format( |
| 186 | + host_uri=self.BRAND_MAPPINGS[self.brand][self.HOST_URI], |
| 187 | + device_set_endpoint=self.DEVICE_SET_ENDPOINT), |
| 188 | + data=payload, |
| 189 | + headers={ |
| 190 | + 'MyQApplicationId': 'JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu', |
| 191 | + 'SecurityToken': self.security_token |
| 192 | + } |
| 193 | + ) |
| 194 | + |
| 195 | + devices.raise_for_status() |
| 196 | + except (NameError, requests.exceptions.HTTPError) as ex: |
| 197 | + self.logger.error("MyQ - API Error %s", ex) |
| 198 | + return False |
| 199 | + |
| 200 | + return device_action.status_code == 200 |
| 201 | + |
0 commit comments