├── README.md └── myq.py /README.md: -------------------------------------------------------------------------------- 1 | ### Beginning in Home Assistant 0.39 this will be an official component. Users using this custom component will need to remove it and also make a simple change to their config. Changing "brand" to "type". The documentation on the HA site will have a simple example. 2 | 3 | # myq 4 | ## MyQ custom component for Home Assistant. 5 | 6 | Initial code provided by user MisterWil on the Home Assistant Forum. His initial code can be found at http://pastebin.com/qVszax1t. 7 | 8 | Place the myq.py file in the /path/to/homeassistant_config/custom_components/cover/ 9 | 10 | > Note: "/path/to/homeassistant_config" should be the path to where your configuration.yaml file is. 11 | 12 | In your configuration.yaml file use an entry similar to: 13 | 14 | ``` 15 | cover: 16 | - platform: myq 17 | username: email@email.com 18 | password: password 19 | brand: chamberlain //liftmaster,chamberlain,craftsman,merlin 20 | ``` 21 | -------------------------------------------------------------------------------- /myq.py: -------------------------------------------------------------------------------- 1 | # Support for MyQ garage doors. 2 | # 3 | # For more details about this platform, please refer to the forum at 4 | # https://community.home-assistant.io/t/myq-componenet-issues/1860/195 5 | 6 | import logging 7 | import requests 8 | 9 | from homeassistant.components.cover import CoverDevice 10 | from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, STATE_OPEN, STATE_CLOSED 11 | 12 | DEPENDENCIES = [] 13 | 14 | CONF_BRAND = 'brand' 15 | 16 | DEFAULT_NAME = 'myq' 17 | 18 | LIFTMASTER = 'liftmaster' 19 | CHAMBERLAIN = 'chamberlain' 20 | CRAFTSMAN = 'craftsman' 21 | MERLIN = 'merlin' 22 | 23 | SUPPORTED_BRANDS = [LIFTMASTER, CHAMBERLAIN, CRAFTSMAN, MERLIN] 24 | SUPPORTED_DEVICE_TYPE_NAMES = ['GarageDoorOpener', 'Garage Door Opener WGDO', 'VGDO'] 25 | 26 | APP_ID = 'app_id' 27 | HOST_URI = 'myqexternal.myqdevice.com' 28 | 29 | BRAND_MAPPINGS = { 30 | LIFTMASTER: { 31 | APP_ID: 'Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB/i' 32 | }, 33 | CHAMBERLAIN: { 34 | APP_ID: 'OA9I/hgmPHFp9RYKJqCKfwnhh28uqLJzZ9KOJf1DXoo8N2XAaVX6A1wcLYyWsnnv' 35 | }, 36 | CRAFTSMAN: { 37 | APP_ID: 'YmiMRRS1juXdSd0KWsuKtHmQvh5RftEp5iewHdCvsNB77FnQbY+vjCVn2nMdIeN8' 38 | }, 39 | MERLIN: { 40 | APP_ID: '3004cac4e920426c823fa6c2ecf0cc28ef7d4a7b74b6470f8f0d94d6c39eb718' 41 | } 42 | } 43 | 44 | def setup_platform(hass, config, add_devices, discovery_info=None): 45 | """Set up the MyQ garage door.""" 46 | 47 | username = config.get(CONF_USERNAME) 48 | password = config.get(CONF_PASSWORD) 49 | 50 | logger = logging.getLogger(__name__) 51 | 52 | if username is None or password is None: 53 | logger.error("MyQ Cover - Missing username or password.") 54 | return 55 | 56 | try: 57 | brand = BRAND_MAPPINGS[config.get(CONF_BRAND)]; 58 | except KeyError: 59 | logger.error("MyQ Cover - Missing or unsupported brand. Supported brands: %s", ', '.join(SUPPORTED_BRANDS)) 60 | return 61 | 62 | myq = MyQAPI(username, password, brand, logger) 63 | 64 | add_devices(MyQCoverDevice(myq, door) for door in myq.get_garage_doors()) 65 | 66 | 67 | class MyQAPI(object): 68 | """Class for interacting with the MyQ iOS App API.""" 69 | 70 | LOGIN_ENDPOINT = "api/v4/User/Validate" 71 | DEVICE_LIST_ENDPOINT = "api/v4/UserDeviceDetails/Get" 72 | DEVICE_SET_ENDPOINT = "api/v4/DeviceAttribute/PutDeviceAttribute" 73 | USERAGENT = "Chamberlain/3773 (iPhone; iOS 10.0.1; Scale/2.00)" 74 | 75 | DOOR_STATE = { 76 | '1': STATE_OPEN, #'open', 77 | '2': STATE_CLOSED, #'close', 78 | '4': STATE_OPEN, #'opening', 79 | '5': STATE_CLOSED, #'closing', 80 | '8': STATE_OPEN, #'in_transition', 81 | '9': STATE_OPEN, #'open' 82 | } 83 | 84 | def __init__(self, username, password, brand, logger): 85 | """Initialize the API object.""" 86 | self.username = username 87 | self.password = password 88 | self.brand = brand 89 | self._logger = logger; 90 | self.security_token = None 91 | self._logged_in = False 92 | 93 | def login(self): 94 | """Log in to the MyQ service.""" 95 | 96 | params = { 97 | 'username': self.username, 98 | 'password': self.password 99 | } 100 | 101 | login = requests.post( 102 | 'https://{host_uri}/{login_endpoint}'.format( 103 | host_uri=HOST_URI, 104 | login_endpoint=self.LOGIN_ENDPOINT), 105 | json=params, 106 | headers={ 107 | 'MyQApplicationId': self.brand[APP_ID], 108 | 'User-Agent': self.USERAGENT 109 | } 110 | ) 111 | 112 | auth = login.json() 113 | self.security_token = auth['SecurityToken'] 114 | self._logger.debug('Logged in to MyQ API') 115 | return True 116 | 117 | def get_devices(self): 118 | """List all MyQ devices.""" 119 | 120 | if not self._logged_in: 121 | self._logged_in = self.login() 122 | 123 | devices = requests.get( 124 | 'https://{host_uri}/{device_list_endpoint}'.format( 125 | host_uri=HOST_URI, 126 | device_list_endpoint=self.DEVICE_LIST_ENDPOINT), 127 | headers={ 128 | 'MyQApplicationId': self.brand[APP_ID], 129 | 'SecurityToken': self.security_token, 130 | 'User-Agent': self.USERAGENT 131 | } 132 | ) 133 | 134 | devices = devices.json()['Devices'] 135 | 136 | return devices 137 | 138 | def get_garage_doors(self): 139 | """List only MyQ garage door devices.""" 140 | 141 | devices = self.get_devices() 142 | 143 | garage_doors = [] 144 | 145 | for device in devices: 146 | if device['MyQDeviceTypeName'] in SUPPORTED_DEVICE_TYPE_NAMES: 147 | dev = {} 148 | for attribute in device['Attributes']: 149 | if attribute['AttributeDisplayName'] == 'desc': 150 | dev['deviceid'] = device['MyQDeviceId'] 151 | dev['name'] = attribute['Value'] 152 | garage_doors.append(dev) 153 | 154 | return garage_doors 155 | 156 | def get_status(self, device_id): 157 | """List only MyQ garage door devices.""" 158 | 159 | devices = self.get_devices() 160 | 161 | for device in devices: 162 | if device['MyQDeviceTypeName'] in SUPPORTED_DEVICE_TYPE_NAMES and device['MyQDeviceId'] == device_id: 163 | dev = {} 164 | for attribute in device['Attributes']: 165 | if attribute['AttributeDisplayName'] == 'doorstate': 166 | garage_state = attribute['Value'] 167 | 168 | garage_state = self.DOOR_STATE[garage_state] 169 | return garage_state 170 | 171 | def close_device(self, device_id): 172 | """Close MyQ Device.""" 173 | return self.set_state(device_id, '0') 174 | 175 | def open_device(self, device_id): 176 | """Open MyQ Device.""" 177 | return self.set_state(device_id, '1') 178 | 179 | def set_state(self, device_id, state): 180 | """Set device state.""" 181 | payload = { 182 | 'attributeName': 'desireddoorstate', 183 | 'myQDeviceId': device_id, 184 | 'AttributeValue': state, 185 | } 186 | device_action = requests.put( 187 | 'https://{host_uri}/{device_set_endpoint}'.format( 188 | host_uri=HOST_URI, 189 | device_set_endpoint=self.DEVICE_SET_ENDPOINT), 190 | data=payload, 191 | headers={ 192 | 'MyQApplicationId': self.brand[APP_ID], 193 | 'SecurityToken': self.security_token, 194 | 'User-Agent': self.USERAGENT 195 | } 196 | ) 197 | 198 | return device_action.status_code == 200 199 | 200 | 201 | class MyQCoverDevice(CoverDevice): 202 | """Representation of a MyQ cover.""" 203 | 204 | def __init__(self, myq, device): 205 | """Initialize with API object, device id""" 206 | self.myq = myq 207 | self.device_id = device['deviceid'] 208 | self._name = device['name'] 209 | self._status = STATE_CLOSED 210 | 211 | @property 212 | def should_poll(self): 213 | """Poll for state.""" 214 | return True 215 | 216 | @property 217 | def name(self): 218 | """Return the name of the garage door if any.""" 219 | return self._name if self._name else DEFAULT_NAME 220 | 221 | @property 222 | def is_closed(self): 223 | """Return True if cover is closed, else False.""" 224 | return self._status == STATE_CLOSED 225 | 226 | def close_cover(self): 227 | """Issue close command to cover.""" 228 | self.myq.close_device(self.device_id) 229 | 230 | def open_cover(self): 231 | """Issue open command to cover.""" 232 | self.myq.open_device(self.device_id) 233 | 234 | def update(self): 235 | self._status = self.myq.get_status(self.device_id) 236 | --------------------------------------------------------------------------------