├── README.md ├── pyAqara ├── __init__.py └── gateway.py └── setup.py /README.md: -------------------------------------------------------------------------------- 1 | # homeassisitant-pyAqara 2 | Home-Assistant plugin for Aqara gateway and devices 3 | 4 | Aqara plugin for [Home-Assistant](https://home-assistant.io/) 5 | 6 | STATUS : PRELIMINARY DRAFT 7 | 8 | Based on the code of snOOrz: 9 | https://github.com/snOOrz/homebridge-aqara 10 | 11 | This repository is contain the core code. 12 | For the custom component refer to efer to the link below 13 | https://github.com/fooxy/homeassistant-aqara 14 | -------------------------------------------------------------------------------- /pyAqara/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fooxy/homeassisitant-pyAqara/dc6d98fa437cce9165b653adc9d834d3e44420dc/pyAqara/__init__.py -------------------------------------------------------------------------------- /pyAqara/gateway.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | import logging 4 | import struct 5 | import collections 6 | import threading 7 | from queue import Queue 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | class AqaraGateway: 12 | 13 | GATEWAY_IP = None 14 | GATEWAY_PORT = None 15 | GATEWAY_SID = None 16 | GATEWAY_TOKEN = None 17 | 18 | MULTICAST_PORT = 9898 19 | GATEWAY_DISCOVERY_PORT = 4321 20 | 21 | MULTICAST_ADDRESS = '224.0.0.50' 22 | SOCKET_BUFSIZE = 1024 23 | 24 | def __init__(self): 25 | self._running = False 26 | self._queue = None 27 | self._timeout = 300 28 | self._devices = collections.defaultdict(list) 29 | self._deviceCallbacks = collections.defaultdict(list) 30 | self.socket = None 31 | self.sids = [] 32 | self.sidsData = [] 33 | 34 | def initGateway(self): 35 | # Send WhoIs in order to get gateway data 36 | cmd_whois = '{"cmd":"whois"}' 37 | resp = self.socketSendMsg(cmd_whois) 38 | self.GATEWAY_IP = resp['ip'] 39 | self.GATEWAY_PORT = int(resp['port']) 40 | self.GATEWAY_SID = resp['sid'] 41 | 42 | cmd_list = '{"cmd":"get_id_list"}' 43 | self.sids = self.socketSendMsg(cmd_list) 44 | 45 | for sid in self.sids: 46 | cmd = '{"cmd":"read", "sid":"' + sid + '"}' 47 | resp = self.socketSendMsg(cmd) 48 | self.sidsData.append({"sid":resp['sid'],"model":resp['model'],"data":json.loads(resp['data'])}) 49 | 50 | self.socket = self._prepare_socket() 51 | 52 | # # Unicast Command 53 | 54 | def socketSendMsg(self, cmd): 55 | 56 | if cmd == '{"cmd":"whois"}': 57 | ip = self.MULTICAST_ADDRESS 58 | port = self.GATEWAY_DISCOVERY_PORT 59 | else: 60 | ip = self.GATEWAY_IP 61 | port = self.GATEWAY_PORT 62 | 63 | tempSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 64 | retryCount = 0 65 | recvData = None 66 | 67 | while retryCount<3 and recvData==None: 68 | try: 69 | tempSocket.settimeout(2 + retryCount*2) 70 | tempSocket.sendto(cmd.encode(), (ip, port)) 71 | tempSocket.settimeout(2 + retryCount*2) 72 | recvData, addr = tempSocket.recvfrom(1024) 73 | if len(recvData) is not None: 74 | decodedJson = recvData.decode() 75 | else: 76 | _LOGGER.error("no response from gateway") 77 | except socket.timeout: 78 | retryCount += 1 79 | _LOGGER.error( 80 | "Timeout on socket - Failed to connect the ip %s, automatic retry", ip) 81 | 82 | tempSocket.close() 83 | 84 | if recvData is not None: 85 | try: 86 | jsonMsg = json.loads(decodedJson) 87 | cmd = jsonMsg['cmd'] 88 | if cmd == 'iam': 89 | return jsonMsg 90 | if cmd == "get_id_list": 91 | return json.loads(jsonMsg['data']) 92 | elif cmd == "get_id_list_ack": 93 | self.GATEWAY_TOKEN = jsonMsg['token'] 94 | devices_SID = json.loads(jsonMsg['data']) 95 | return devices_SID 96 | elif cmd in ["read_ack","write_ack"]: 97 | if self._running: 98 | self._queue.put(jsonMsg) 99 | return jsonMsg 100 | else: 101 | _LOGGER.info("Got unknown response: %s", decodedJson) 102 | except: 103 | _LOGGER.error("Aqara Gateway Failed to manage the json") 104 | else: 105 | _LOGGER.error("Maximum retry times exceed: %s", retryCount) 106 | return None 107 | 108 | def sendCmd(self, cmd): 109 | IP = self.GATEWAY_IP 110 | PORT = self.GATEWAY_PORT 111 | sSocket = self.serverSocket 112 | # print('sendCmd - for IP: ',IP,'with PORT: ',PORT,'with CMD: ',cmd) 113 | try: 114 | sSocket.settimeout(5.0) 115 | sSocket.sendto(cmd.encode("utf-8"), (IP, PORT )) 116 | except socket.timeout: 117 | _LOGGER.error( 118 | "Timeout on socket - Failed to connect the ip %s", IP) 119 | 120 | # # Multicast Command 121 | 122 | def _prepare_socket(self): 123 | # sock = socket.socket(socket.AF_INET, # Internet 124 | # socket.SOCK_DGRAM, # UDP 125 | # socket.IPPROTO_UDP) 126 | 127 | sock = socket.socket(socket.AF_INET, # Internet 128 | socket.SOCK_DGRAM) # UDP 129 | 130 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 131 | 132 | # try: 133 | # sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 134 | # except AttributeError: 135 | # pass # Some systems don't support SO_REUSEPORT 136 | 137 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32) 138 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) 139 | 140 | mreq = struct.pack("=4sl", socket.inet_aton(self.MULTICAST_ADDRESS),socket.INADDR_ANY) 141 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF,self.SOCKET_BUFSIZE) 142 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) 143 | 144 | sock.bind(("0.0.0.0", self.MULTICAST_PORT)) 145 | 146 | return sock 147 | 148 | def send_command(self, cmd): 149 | """Send a command to the UDP subject (all related will answer).""" 150 | self.socket.sendto(json.dumps(cmd).encode("utf-8"), 151 | (self.MULTICAST_ADDRESS, self.MULTICAST_PORT)) 152 | 153 | # # MutliThreading 154 | 155 | def register(self, callbackID, callback): 156 | """Register a callback. 157 | device: device to be updated by subscription 158 | callback: callback for notification of changes 159 | """ 160 | if not callbackID: 161 | _LOGGER.error("Received an invalid device") 162 | return 163 | 164 | _LOGGER.info("Subscribing to events for %s", callbackID) 165 | # self._devices[deviceSID].append(deviceSID) 166 | self._deviceCallbacks[callbackID].append((callback)) 167 | 168 | def _log(self, msg): 169 | """Internal log errors.""" 170 | try: 171 | self._logger.error(msg) 172 | except Exception: # pylint: disable=broad-except 173 | print('ERROR: ' + msg) 174 | 175 | def _callback_thread(self): 176 | """Process callbacks from the queue populated by &listen.""" 177 | while self._running: 178 | packet = self._queue.get(True) 179 | if isinstance(packet, dict): 180 | cmd = packet['cmd'] 181 | sid = packet['sid'] 182 | model = packet['model'] 183 | data = packet['data'] 184 | try: 185 | if 'token' in packet: 186 | self.GATEWAY_TOKEN = packet['token'] 187 | if cmd == 'iam': 188 | print('iam') 189 | elif cmd == 'get_id_list_ack': 190 | self.sids = json.loads(data) 191 | elif cmd in ["heartbeat", "report", "read_ack"]: 192 | if model == 'sensor_ht': 193 | for sensor in ('temperature', 'humidity'): 194 | callback_id = '{} {}'.format(sensor, sid) 195 | for deviceCallback in self._deviceCallbacks.get(callback_id, ()): 196 | deviceCallback(model, 197 | sid, 198 | cmd, 199 | json.loads(data)) 200 | else: 201 | for deviceCallback in self._deviceCallbacks.get(sid, ()): 202 | deviceCallback(model, 203 | sid, 204 | cmd, 205 | json.loads(data)) 206 | elif cmd == 'write_ack': 207 | for deviceCallback in self._deviceCallbacks.get(sid, ()): 208 | deviceCallback(model, 209 | sid, 210 | cmd, 211 | json.loads(data)) 212 | 213 | except Exception as err: # pylint: disable=broad-except 214 | self._log("Exception in aqara gateway callback\nType: " + 215 | str(type(err)) + "\nMessage: " + str(err)) 216 | self._queue.task_done() 217 | 218 | def _listen_thread(self): 219 | """The main &listen loop.""" 220 | # print('gateway _listen_thread()') 221 | while self._running: 222 | if self.socket is not None: 223 | data, addr = self.socket.recvfrom(self.SOCKET_BUFSIZE) 224 | try: 225 | payload = json.loads(data.decode("utf-8")) 226 | # print('gateway listen_thread() - payload: ',payload) 227 | self._queue.put(payload) 228 | except Exception as e: 229 | raise 230 | _LOGGER.error("Can't handle message %r (%r)" % (data, e)) 231 | 232 | self._queue.put({}) # empty item to ensure callback thread shuts down 233 | 234 | def stop(self): 235 | """Stop listening.""" 236 | self._running = False 237 | self.socket.close() 238 | self.socket = None 239 | 240 | def listen(self, timeout=(5, 300)): 241 | """Start the &listen long poll and return immediately.""" 242 | # print('gateway listen()') 243 | if self._running: 244 | return False 245 | self._queue = Queue() 246 | self._running = True 247 | self._timeout = timeout 248 | threading.Thread(target=self._listen_thread, args=()).start() 249 | threading.Thread(target=self._callback_thread, args=()).start() 250 | return True 251 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='pyAqara', 4 | version='0.41', 5 | description='Home-Assistant component for Aqara gateway integration', 6 | keywords='aqara gateway xiaomi lumi hub', 7 | author='fooxy', 8 | author_email='yadir.rabir@gmail.com', 9 | url='https://github.com/fooxy', 10 | zip_safe=False, 11 | platforms=["any"], 12 | packages=find_packages(), 13 | ) 14 | --------------------------------------------------------------------------------