├── requirements.txt ├── Dockerfile ├── README.md └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | flask 3 | flask-restful -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | RUN apt-get update && apt-get -y install python python-pip 3 | ADD . /opt/lutron 4 | WORKDIR /opt/lutron 5 | RUN pip install -r requirements.txt 6 | EXPOSE 5000 7 | CMD python main.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lutron REST API 2 | ============= 3 | 4 | This is simple Flask based API for interacting with Lutron Caseta 5 | 6 | This was originally written specifically for controlling Lutron Shades, but can easily 7 | be modified to work with any Lutron Device. 8 | 9 | 10 | ## Installing 11 | 12 | After downloading, install using setuptools. 13 | 14 | pip install -r requirements.txt 15 | python main.py 16 | 17 | ## Docker 18 | 19 | 20 | The latest build of this project is also available as a Docker image from Docker Hub 21 | 22 | docker pull kecorbin/lutron-shades-api 23 | sudo docker run -d --restart=always -e LUTRON_HOST= --name shades-api --net=host shades-api 24 | 25 | ## Usage 26 | 27 | ### Macros 28 | 29 | 30 | Open the shades 31 | 32 | curl -X POST 127.0.0.1:5000/shades/open 33 | 34 | Close the shades 35 | 36 | curl -X POST 127.0.0.1:5000/shades/close 37 | 38 | Set the shades at 50% 39 | 40 | curl -X POST 127.0.0.1:5000/shades/50 41 | 42 | 43 | ### Device Command Examples 44 | 45 | ``` 46 | Start Lowering a Device Group 47 | 48 | 49 | /api/device/6/6/3 - Equivalent to #DEVICE,6,6,3 50 | 51 | Stop Lowering a Device Group 52 | 53 | /api/device/6/6/4 - Equivalent to #DEVICE,6,6,4 54 | 55 | Start Raising a Device Group 56 | 57 | #DEVICE,6,5,3 58 | /api/device/6/5/3 59 | 60 | Stop Lowering a Device Group 61 | 62 | #DEVICE,6,5,4 63 | /api/device/6/5/4 64 | 65 | ``` 66 | 67 | 68 | ## Smarthings Integration 69 | 70 | Once you have your API up and running, integrating with other hubs is super simple. 71 | 72 | Checkout [https://github.com/kecorbin/smartthings](https://github.com/kecorbin/smartthings) for a sample using Samsung Smartthings 73 | 74 | ## Home Assistant Integration 75 | 76 | This can also be used to integrate Lutron into Home Assistant, using the shades example from above, the following can be 77 | specified in your configuration.yaml file for HASS. 78 | 79 | 80 | - platform: command_line 81 | rollershutters: 82 | Living Room Rollershutter: 83 | upcmd: curl -X POST http://192.168.10.25:5000/shades/open 84 | downcmd: curl -X POST http://192.168.10.25:5000/shades/close 85 | stopcmd: curl -X POST http://192.168.10.25:5000/shades/50 86 | 87 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import telnetlib 3 | import time 4 | import requests 5 | import time 6 | from flask import Flask 7 | from flask_restful import Resource, Api 8 | import argparse 9 | 10 | import sys 11 | 12 | 13 | 14 | app = Flask(__name__) 15 | api = Api(app) 16 | 17 | 18 | _NEWLINE = '\n' 19 | 20 | # This is ugly but for now we will poll all the devices we know about 21 | # TODO dynamically populate this list 22 | 23 | # 2,3,4,5 are the individual shades - 6 represents a group configured in Lutron App 24 | ACTIVE_SHADES = [2,3,4,5] 25 | 26 | def get_lutron_host(): 27 | """ 28 | Helper to obtain LUTRON host 29 | 30 | :return: str lutron bridge IP address 31 | """ 32 | # First we will attempt to get it from the current environment 33 | 34 | if not LUTRON_HOST: 35 | # If we don't have it yet - we will check command line arguments 36 | pass 37 | 38 | return None 39 | 40 | LUTRON_HOST = os.getenv('LUTRON_HOST') 41 | 42 | LUTRON_PORT = 23 43 | LUTRON_USERNAME = 'lutron' 44 | LUTRON_PASSWORD = 'integration' 45 | 46 | 47 | def run_async(func): 48 | """ 49 | run_async(func) 50 | function decorator, intended to make "func" run in a separate 51 | thread (asynchronously). 52 | Returns the created Thread object 53 | 54 | E.g.: 55 | @run_async 56 | def task1(): 57 | do_something 58 | 59 | @run_async 60 | def task2(): 61 | do_something_too 62 | 63 | t1 = task1() 64 | t2 = task2() 65 | ... 66 | t1.join() 67 | t2.join() 68 | """ 69 | from threading import Thread 70 | from functools import wraps 71 | 72 | @wraps(func) 73 | def async_func(*args, **kwargs): 74 | func_hl = Thread(target=func, args=args, kwargs=kwargs) 75 | func_hl.start() 76 | return func_hl 77 | 78 | return async_func 79 | 80 | 81 | def login(): 82 | connection = False 83 | session = telnetlib.Telnet(LUTRON_HOST, LUTRON_PORT) 84 | while connection is False: 85 | print 'Attempting to connect to Lutron Hub' 86 | session.read_until("login:") 87 | session.write('lutron\r\n') 88 | session.read_until("password") 89 | session.write('integration\r\n') 90 | prompt = session.read_until('GNET') 91 | connection = True 92 | print "Successfully Logged in to Lutron Hub" 93 | return session 94 | 95 | 96 | @run_async 97 | def open(session): 98 | """ 99 | Helper function to "cascade" the opening/closing of shades 100 | :param session: telnetlib.Telnet authenticated session 101 | :return: 102 | """ 103 | session.write('#OUTPUT,2,1,100\r\n') 104 | time.sleep(2) 105 | session.write('#OUTPUT,3,1,100\r\n') 106 | time.sleep(2) 107 | session.write('#OUTPUT,4,1,100\r\n') 108 | time.sleep(2) 109 | session.write('#OUTPUT,5,1,100\r\n') 110 | time.sleep(2) 111 | 112 | def send_lutron_command(session, command, integration, action, parameters): 113 | session.write('#{},{},{},{}\r\n'.format(command, integration, action, parameters or 0)) 114 | return "OK" 115 | 116 | 117 | @run_async 118 | def close(session): 119 | """ 120 | Helper function to "cascade" the opening/closing of shades 121 | :param session: telnetlib.Telnet authenticated session 122 | :return: 123 | """ 124 | session.write('#OUTPUT,5,1,0\r\n') 125 | time.sleep(2) 126 | session.write('#OUTPUT,4,1,0\r\n') 127 | time.sleep(2) 128 | session.write('#OUTPUT,3,1,0\r\n') 129 | time.sleep(2) 130 | session.write('#OUTPUT,2,1,0\r\n') 131 | time.sleep(2) 132 | 133 | 134 | def get_status(session, device_id='3'): 135 | """ 136 | Return the status of a device 137 | :param session: telnetlib.Telnet authenticated session 138 | :return: 139 | """ 140 | if isinstance(device_id, int): 141 | device_id = str(device_id) 142 | print "Getting Status for device_id={}".format(device_id) 143 | session.write('?OUTPUT,{},1\r\n'.format(device_id)) 144 | prompt = session.expect(["OUTPUT,\d+,1,\d+.\d+"], 3) 145 | if prompt[1] is not None: 146 | prompt = prompt[1].string 147 | try: 148 | val = int(float(prompt.split(',1,')[1].split('\r')[0])) 149 | except: 150 | val = 0 151 | return val 152 | else: 153 | return 0 154 | 155 | 156 | 157 | @run_async 158 | def set_level(session, id, level): 159 | session.write('#OUTPUT,{},1,{}\r\n'.format(id, level)) 160 | time.sleep(2) 161 | 162 | 163 | class Status(Resource): 164 | def get(self): 165 | session = login() 166 | 167 | resp = {'devices': {}} 168 | for id in ACTIVE_SHADES: 169 | value = get_status(session, device_id=id) 170 | entry = {"value": int(value)} 171 | resp['devices'][str(id)] = entry 172 | return resp 173 | 174 | class DeviceStatus(Resource): 175 | def get(self, integration): 176 | session = login() 177 | resp = get_status(session, device_id=integration) 178 | print resp 179 | return {'status': resp} 180 | 181 | 182 | class ShadesOpen(Resource): 183 | def post(self): 184 | session = login() 185 | open(session) 186 | 187 | return {'status': 'open'} 188 | 189 | 190 | class ShadesClose(Resource): 191 | def post(self): 192 | session = login() 193 | close(session) 194 | 195 | return {'status': 'closed'} 196 | 197 | 198 | class ShadesLevel(Resource): 199 | """ 200 | This is a macro endpoint that sets a group of shades at the same level 201 | """ 202 | def post(self, level): 203 | session = login() 204 | 205 | set_level(session, '2', level) 206 | set_level(session, '3', level) 207 | set_level(session, '4', level) 208 | set_level(session, '5', level) 209 | 210 | return {'status': level} 211 | 212 | class ShadesStatus(Resource): 213 | def get(self): 214 | session = login() 215 | 216 | resp = {'devices': {}} 217 | for id in ACTIVE_SHADES: 218 | value = get_status(session, device_id=id) 219 | entry = {"level": int(value)} 220 | resp['devices'][str(id)] = entry 221 | return resp 222 | 223 | 224 | class Command(Resource): 225 | """ 226 | Generic API resource for interacting with Lutron devices. The primary interface is via the command for 227 | controlling on/off status of Lutron devices. 228 | 229 | Example Usages 230 | 231 | Start Lowering a Device Group 232 | 233 | #DEVICE,6,6,3 234 | /api/device/6/6/3 235 | 236 | Stop Lowering a Device Group 237 | #DEVICE,6,6,4 238 | /api/device/6/6/4 239 | 240 | Start Raising a Device Group 241 | 242 | #DEVICE,6,5,3 243 | /api/device/6/5/3 244 | 245 | Stop Lowering a Device Group 246 | 247 | #DEVICE,6,5,4 248 | /api/device/6/5/4 249 | 250 | #OUTPUT,3,1,100 251 | /api/output/3/1/100 252 | 253 | """ 254 | 255 | def get(self, command, integration, action, parameters): 256 | return { 257 | 'commnad': command, 258 | 'integration': integration, 259 | 'action': action, 260 | 'parameters': parameters 261 | } 262 | 263 | def post(self, command, integration, action, parameters): 264 | session = login() 265 | send_lutron_command(session, command, integration, action, parameters) 266 | # Need better validation here, for now we assume the command worked 267 | return { 268 | 'integration': integration, 269 | 'action': action, 270 | 'parameters': parameters 271 | } 272 | 273 | # Macro endpoints 274 | api.add_resource(ShadesStatus, '/shades') 275 | api.add_resource(ShadesLevel, '/shades/') 276 | api.add_resource(ShadesOpen, '/shades/open') 277 | api.add_resource(ShadesClose, '/shades/close') 278 | 279 | # Expose Lutron commands directly as API 280 | api.add_resource(Command, '/api////') 281 | api.add_resource(Status, '/api//') 282 | #api.add_resource(Status, '/api/output') 283 | 284 | if __name__ == '__main__': 285 | 286 | app.run(host='0.0.0.0', debug=True) 287 | --------------------------------------------------------------------------------