├── LICENSE ├── README.md └── orvibo ├── __init__.py └── orvibo.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 cherezov 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # orvibo 2 | Module to manipulate Orvibo devices, such as [s20 wifi sockets](http://www.aliexpress.com/item/Orvibo-S20-Wifi-Cell-Phone-Power-Socket-Wireless-Timer-Switch-Wall-Plug-Phone-Wireless-Remote-Control/32357053063.html) and [AllOne IR blasters](http://www.aliexpress.com/item/Orvibo-Allone-Wiwo-R1-Intelligent-house-Control-Center-Smart-Home-WIFI-IR-RF-Wireless-Remote-Switch/32247638788.html) to control whatever devices via emiting IR signal (TVs, AV receivers, air conditioners, etc) 3 | 4 | 5 | ## Refferences 6 | * Lots of info was found in [ninja-allone](https://github.com/Grayda/ninja-allone/blob/master/lib/allone.js) library 7 | * S20 data analysis by anonymous is [here](http://pastebin.com/0w8N7AJD) 8 | * [python-orvibo](https://github.com/happyleavesaoc/python-orvibo) similar module, but currently supports Orvibo S20 sockets only. 9 | 10 | ## TODO 11 | - [x] Get rid of keeping connection to the Orvibo device, which breaks further discover 12 | - [x] Test under linux platform 13 | - [x] Consider python 2.7 support 14 | - [x] Add mac and type cmd line arguments in order decrease execution latency (ver 1.1) 15 | - [ ] API for adding new Orvibo device to the network 16 | - [ ] Python setup script 17 | - [ ] Orvibo s20 event handler 18 | - [ ] ~~Learning and emiting RF 433MHz signal~~ [Issue#7](https://github.com/cherezov/orvibo/issues/7) 19 | 20 | ## Requires 21 | * Python (tested on Win7 with python 2.7 and python 3.4 and Ubuntu with python 3.2) 22 | 23 | ## Known applications 24 | *please let me know about apps not listed here :octocat:* 25 | * [OpenHAB](https://community.openhab.org/t/orvibo-allone-ir-blaster-guide/9111) guide by community user [@robinson](https://community.openhab.org/users/robinson) 26 | * [Samsung Artic](https://www.artik.io/works-with-cloud/orbivo-wiwo/) guide cloud setup 27 | * [Domoticz](http://easydomoticz.com/forum/viewtopic.php?t=2406) community guide 28 | 29 | ## Usage 30 | ### As console app 31 | #### Overview 32 | ```shell 33 | orvibo.py [-i ] | [-m -x ] [-s on|off] | [-t ] | [-e ]] 34 | ``` 35 | #### Discover all Orvibo devices in the network 36 | ```shell 37 | > python orvibo.py 38 | Orvibo[type=socket, ip=192.168.1.45, mac='acdf238d1d2e'] 39 | Orvibo[type=irda, ip=192.168.1.37, mac='accf4378efdc'] 40 | ``` 41 | #### Discover device by ip 42 | ```shell 43 | > python orvibo.py -i 192.168.1.45 44 | Orvibo[type=socket, ip=192.168.1.45, mac='acdf238d1d2e'] 45 | Is enabled: True 46 | ``` 47 | #### State of known device [draft] 48 | Faster than just discovering by ip, since discovering step is skipped here. 49 | *Note*: There is no any mac/type validation! 50 | ```shell 51 | > python orvibo.py -m acdf238d1d2e -x socket 52 | Orvibo[type=socket, ip=unknown, mac='acdf238d1d2e'] 53 | Is enabled: True 54 | ``` 55 | Same arguments can be used for all examples below. 56 | #### Switch s20 wifi socket 57 | ```shell 58 | > python orvibo.py -i 192.168.1.45 -s on 59 | Orvibo[type=socket, ip=192.168.1.45, mac='acdf238d1d2e'] 60 | Already enabled. 61 | ``` 62 | #### Grab IR code to file 63 | ```shell 64 | > python orvibo.py -i 192.168.1.37 -t test.ir 65 | Orvibo[type=irda, ip=192.168.1.37, mac='accf5378dfdc'] 66 | Done 67 | ``` 68 | #### Emit already grabed IR code 69 | ```shell 70 | > python orvibo.py -i 192.168.1.37 -e test.ir 71 | Orvibo[type=irda, ip=192.168.1.37, mac='accf5378dfdc'] 72 | Done 73 | ``` 74 | ### As python module 75 | #### Discover all devices in the network 76 | ```python 77 | from orvibo import Orvibo 78 | 79 | for ip in Orvibo.discover(): 80 | print('IP =', ip) 81 | 82 | for args in Orvibo.discover().values(): 83 | device = Orvibo(*args) 84 | print(device) 85 | ``` 86 | Result 87 | ``` 88 | IP = 192.168.1.45 89 | IP = 192.168.1.37 90 | Orvibo[type=socket, ip=192.168.1.45, mac='acdf238d1d2e'] 91 | Orvibo[type=irda, ip=192.168.1.37, mac='accf4378efdc'] 92 | ``` 93 | 94 | #### Getting exact device by IP 95 | ```python 96 | device = Orvibo.discover('192.168.1.45') 97 | print(device) 98 | ``` 99 | Result: 100 | ``` 101 | Orvibo[type=socket, ip=192.168.1.45, mac='acdf238d1d2e'] 102 | ``` 103 | 104 | #### Control S20 wifi socket 105 | **only for devices with type 'socket'** 106 | ```python 107 | device = Orvibo('192.168.1.45') 108 | print('Is socket enabled: {}'.format(device.on)) 109 | device.on = not device.on # Toggle socket 110 | print('Is socket enabled: {}'.format(device.on)) 111 | ``` 112 | Result: 113 | ``` 114 | Is socket enabled: True 115 | Is socket enabled: False 116 | ``` 117 | 118 | #### Learning AllOne IR blaster 119 | **only for devices with type 'irda'** 120 | ```python 121 | device = Orvibo('192.168.1.37') 122 | ir = device.learn('test.ir', timeout=15) # AllOne red light is present, 123 | # waiting for ir signal for 15 seconds and stores it to test.ir file 124 | if ir is not None: 125 | # Emit IR code from "test.ir" 126 | device.emit('test.ir') 127 | # Or with the same result 128 | # device.emit(ir) 129 | ``` 130 | #### Keeping connection to Orvibo device 131 | 132 | By default module doesn't keep connection to the Orvibo device to allow user not thinking about unplanned disconnections from device by whatever reasons (power outage, wifi router reboot, etc). Such behavior actually leads to valuable delay between sending request and applying command on the Orvibo device. Module allows to keep the connection and decrease the latency via setting keep_connection property to True. In this way closing connection and handling socket errors duties lie on orvibo python library user. 133 | 134 | ```python 135 | import socket 136 | 137 | device = Orvibo('192.168.1.45') 138 | device.keep_connection = True 139 | 140 | def blink_socket(device): 141 | try: 142 | device.on = not device.on 143 | except socket.error: 144 | # Reset connection to device 145 | device.keep_connection = True 146 | 147 | try: 148 | # Note: now socket errors are handled by user! 149 | 150 | # Blink the socket ^_^ 151 | for i in range(5): 152 | blink_socket(device) 153 | 154 | # You also may stop using connection anytime 155 | # device.keep_connection = False 156 | finally: 157 | # Connection must be closed explicitly. 158 | # via 159 | device.close() 160 | # or via 161 | # device.keep_connection = False 162 | ``` 163 | -------------------------------------------------------------------------------- /orvibo/__init__.py: -------------------------------------------------------------------------------- 1 | from orvibo.orvibo import * 2 | -------------------------------------------------------------------------------- /orvibo/orvibo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # @file orvibo.py 4 | # @author cherezov.pavel@gmail.com 5 | 6 | # Change log: 7 | # 1.0 Initial stable version 8 | # 1.1 Mac and type arguments introduced for fast control known devices 9 | # 1.2 Python3 discover bug fixed 10 | # 1.3 ip argument is now optional in case of mac and type are passed 11 | # 1.4 keep connection functionality implemented 12 | # 1.4.1 Learn/Emit logging improved 13 | # 1.5 Learn/Emit Orvibo SmartSwitch RF433 MHz signal support added 14 | __version__ = "1.5" 15 | 16 | from contextlib import contextmanager 17 | import logging 18 | import struct 19 | import select 20 | import random 21 | import socket 22 | import binascii 23 | import time 24 | import sys 25 | 26 | py3 = sys.version_info[0] == 3 27 | 28 | BROADCAST = '255.255.255.255' 29 | PORT = 10000 30 | 31 | MAGIC = b'\x68\x64' 32 | SPACES_6 = b'\x20\x20\x20\x20\x20\x20' 33 | ZEROS_4 = b'\x00\x00\x00\x00' 34 | 35 | ON = b'\x01' 36 | OFF = b'\x00' 37 | 38 | # CMD CODES 39 | DISCOVER = b'\x71\x61' 40 | DISCOVER_RESP = DISCOVER 41 | 42 | SUBSCRIBE = b'\x63\x6c' 43 | SUBSCRIBE_RESP = SUBSCRIBE 44 | 45 | CONTROL = b'\x64\x63' 46 | CONTROL_RESP = CONTROL 47 | 48 | SOCKET_EVENT = b'\x73\x66' # something happend with socket 49 | 50 | LEARN_IR = b'\x6c\x73' 51 | LEARN_IR_RESP = LEARN_IR 52 | 53 | BLAST_IR = b'\x69\x63' 54 | 55 | BLAST_RF433 = CONTROL 56 | LEARN_RF433 = CONTROL 57 | 58 | class OrviboException(Exception): 59 | """ Module level exception class. 60 | """ 61 | def __init__(self, msg): 62 | super(OrviboException, self).__init__(msg) 63 | 64 | def _reverse_bytes(mac): 65 | """ Helper method to reverse bytes order. 66 | 67 | mac -- bytes to reverse 68 | """ 69 | ba = bytearray(mac) 70 | ba.reverse() 71 | return bytes(ba) 72 | 73 | def _random_byte(): 74 | """ Generates random single byte. 75 | """ 76 | return bytes([int(256 * random.random())]) 77 | 78 | def _random_n_bytes(n): 79 | res = b'' 80 | for n in range(n): 81 | res += _random_byte() 82 | return res 83 | 84 | def _packet_id(): 85 | return _random_n_bytes(2) 86 | 87 | _placeholders = ['MAGIC', 'SPACES_6', 'ZEROS_4', 'CONTROL', 'CONTROL_RESP', 'SUBSCRIBE', 'LEARN_IR', 'BLAST_RF433', 'BLAST_IR', 'DISCOVER', 'DISCOVER_RESP' ] 88 | def _debug_data(data): 89 | data = binascii.hexlify(bytearray(data)) 90 | for s in _placeholders: 91 | p = binascii.hexlify(bytearray( globals()[s])) 92 | data = data.replace(p, b" + " + s.encode() + b" + ") 93 | return data[3:] 94 | 95 | def _parse_discover_response(response): 96 | """ Extracts MAC address and Type of the device from response. 97 | 98 | response -- dicover response, format: 99 | MAGIC + LENGTH + DISCOVER_RESP + b'\x00' + MAC + SPACES_6 + REV_MAC + ... TYPE 100 | """ 101 | header_len = len(MAGIC + DISCOVER_RESP) + 2 + 1 # 2 length bytes, and 0x00 102 | mac_len = 6 103 | spaces_len = len(SPACES_6) 104 | 105 | mac_start = header_len 106 | mac_end = mac_start + mac_len 107 | mac = response[mac_start:mac_end] 108 | 109 | type = None 110 | if b'SOC' in response: 111 | type = Orvibo.TYPE_SOCKET 112 | 113 | elif b'IRD' in response: 114 | type = Orvibo.TYPE_IRDA 115 | 116 | return (type, mac) 117 | 118 | def _create_orvibo_socket(ip=''): 119 | """ Creates socket to talk with Orvibo devices. 120 | 121 | Arguments: 122 | ip - ip address of the Orvibo device or empty string in case of broadcasting discover packet. 123 | """ 124 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 125 | for opt in [socket.SO_BROADCAST, socket.SO_REUSEADDR, socket.SO_BROADCAST]: 126 | sock.setsockopt(socket.SOL_SOCKET, opt, 1) 127 | if ip: 128 | sock.connect((ip, PORT)) 129 | else: 130 | sock.bind((ip, PORT)) 131 | return sock 132 | 133 | @contextmanager 134 | def _orvibo_socket(external_socket = None): 135 | sock = _create_orvibo_socket() if external_socket is None else external_socket 136 | 137 | yield sock 138 | 139 | if external_socket is None: 140 | sock.close() 141 | else: 142 | pass 143 | 144 | class Packet: 145 | """ Represents response sender/recepient address and binary data. 146 | """ 147 | 148 | Request = 'request' 149 | Response = 'response' 150 | 151 | def __init__(self, ip = BROADCAST, data = None, type = Request): 152 | self.ip = ip 153 | self.data = data 154 | self.type = type 155 | 156 | def __repr__(self): 157 | return 'Packet {} {}: {}'.format('to' if self.type == self.Request else 'from', self.ip, _debug_data(self.data)) 158 | 159 | @property 160 | def cmd(self): 161 | """ 2 bytes command of the orvibo packet 162 | """ 163 | if self.data is None: 164 | return b'' 165 | return self.data[4:6] 166 | 167 | @property 168 | def length(self): 169 | """ 2 bytes command of the orvibo packet 170 | """ 171 | if self.data is None: 172 | return b'' 173 | return self.data[2:4] 174 | 175 | 176 | def send(self, sock, timeout = 10): 177 | """ Sends binary packet via socket. 178 | 179 | Arguments: 180 | sock -- socket to send through 181 | packet -- byte string to send 182 | timeout -- number of seconds to wait for sending operation 183 | """ 184 | if self.data is None: 185 | # Nothing to send 186 | return 187 | 188 | for i in range(timeout): 189 | r, w, x = select.select([], [sock], [sock], 1) 190 | if sock in w: 191 | sock.sendto(bytearray(self.data), (self.ip, PORT)) 192 | elif sock in x: 193 | raise OrviboException("Failed while sending packet.") 194 | else: 195 | # nothing to send 196 | break 197 | 198 | @staticmethod 199 | def recv(sock, expectResponseType = None, timeout = 10): 200 | """ Receive first packet from socket of given type 201 | 202 | Arguments: 203 | sock -- socket to listen to 204 | expectResponseType -- 2 bytes packet command type to filter result data 205 | timeout -- number of seconds to wait for response 206 | """ 207 | response = None 208 | for i in range(10): 209 | r, w, x = select.select([sock], [], [sock], 1) 210 | if sock in r: 211 | data, addr = sock.recvfrom(1024) 212 | 213 | if expectResponseType is not None and data[4:6] != expectResponseType: 214 | continue 215 | 216 | response = Packet(addr[0], data, Packet.Response) 217 | break 218 | elif sock in x: 219 | raise OrviboException('Getting response failed') 220 | else: 221 | # Nothing to read 222 | break 223 | 224 | return response 225 | 226 | @staticmethod 227 | def recv_all(sock, expectResponseType = None, timeout = 10): 228 | res = None 229 | while True: 230 | resp = Packet.recv(sock, expectResponseType, timeout) 231 | if resp is None: 232 | break 233 | res = resp 234 | return res 235 | 236 | def compile(self, *args): 237 | """ Assemblies packet to send to orvibo device. 238 | 239 | *args -- number of bytes strings that will be concatenated, and prefixed with MAGIC heaer and packet length. 240 | """ 241 | 242 | length = len(MAGIC) + 2 # len itself 243 | packet = b'' 244 | for a in args: 245 | length += len(a) 246 | packet += a 247 | 248 | msg_len_2 = struct.pack('>h', length) 249 | self.data = MAGIC + msg_len_2 + packet 250 | return self 251 | 252 | class Orvibo(object): 253 | """ Represents Orvibo device, such as wifi socket (TYPE_SOCKET) or AllOne IR blaster (TYPE_IRDA) 254 | """ 255 | 256 | TYPE_SOCKET = 'socket' 257 | TYPE_IRDA = 'irda' 258 | 259 | def __init__(self, ip, mac = None, type = 'Unknown'): 260 | self.ip = ip 261 | self.type = type 262 | self.__last_subscr_time = time.time() - 1 # Orvibo doesn't like subscriptions frequently that 1 in 0.1sec 263 | self.__logger = logging.getLogger('{}@{}'.format(self.__class__.__name__, ip)) 264 | self.__socket = None 265 | self.mac = mac 266 | 267 | # TODO: make this tricky code clear 268 | if py3 and isinstance(mac, str): 269 | self.mac = binascii.unhexlify(mac) 270 | else: 271 | try: 272 | self.mac = binascii.unhexlify(mac) 273 | except: 274 | pass 275 | 276 | if mac is None: 277 | self.__logger.debug('MAC address is not provided. Discovering..') 278 | d = Orvibo.discover(self.ip) 279 | self.mac = d.mac 280 | self.type = d.type 281 | 282 | def __del__(self): 283 | self.close() 284 | 285 | def close(self): 286 | if self.__socket is not None: 287 | try: 288 | self.__socket.close() 289 | except socket.error: 290 | # socket seems not alive 291 | pass 292 | self.__socket = None 293 | 294 | @property 295 | def keep_connection(self): 296 | """ Keeps connection to the Orvibo device. 297 | """ 298 | return self.__socket is not None 299 | 300 | @keep_connection.setter 301 | def keep_connection(self, value): 302 | """ Keeps connection to the Orvibo device. 303 | """ 304 | # Close connection if alive 305 | self.close() 306 | 307 | if value: 308 | self.__socket = _create_orvibo_socket(self.ip) 309 | if self.__subscribe(self.__socket) is None: 310 | raise OrviboException('Connection subscription error.') 311 | else: 312 | self.close() 313 | 314 | def __repr__(self): 315 | mac = binascii.hexlify(bytearray(self.mac)) 316 | return "Orvibo[type={}, ip={}, mac={}]".format(self.type, 'Unknown' if self.ip == BROADCAST else self.ip, mac.decode('utf-8') if py3 else mac) 317 | 318 | @staticmethod 319 | def discover(ip = None): 320 | """ Discover all/exact devices in the local network 321 | 322 | Arguments: 323 | ip -- ip address of the discovered device 324 | 325 | returns -- map {ip : (ip, mac, type)} of all discovered devices if ip argument is None 326 | Orvibo object that represents device at address ip. 327 | raises -- OrviboException if requested ip not found 328 | """ 329 | devices = {} 330 | with _orvibo_socket() as s: 331 | logger = logging.getLogger(Orvibo.__class__.__name__) 332 | logger.debug('Discovering Orvibo devices') 333 | discover_packet = Packet(BROADCAST) 334 | discover_packet.compile(DISCOVER) 335 | discover_packet.send(s) 336 | 337 | for indx in range(512): # supposer there are less then 512 devices in the network 338 | p = discover_packet.recv(s) 339 | if p is None: 340 | # No more packets in the socket 341 | break 342 | 343 | orvibo_type, orvibo_mac = _parse_discover_response(p.data) 344 | logger.debug('Discovered values: type={}, mac={}'.format(orvibo_type, orvibo_mac)); 345 | 346 | if not orvibo_mac: 347 | # Filter ghosts devices 348 | continue 349 | 350 | devices[p.ip] = (p.ip, orvibo_mac, orvibo_type) 351 | 352 | if ip is None: 353 | return devices 354 | 355 | if ip not in devices.keys(): 356 | raise OrviboException('Device ip={} not found in {}.'.format(ip, devices.keys())) 357 | 358 | return Orvibo(*devices[ip]) 359 | 360 | def subscribe(self): 361 | """ Subscribe to device. 362 | 363 | returns -- last response byte, which represents device state 364 | """ 365 | with _orvibo_socket(self.__socket) as s: 366 | return self.__subscribe(s) 367 | 368 | def __subscribe(self, s): 369 | """ Required action after connection to device before sending any requests 370 | 371 | Arguments: 372 | s -- socket to use for subscribing 373 | 374 | returns -- last response byte, which represents device state 375 | """ 376 | 377 | if time.time() - self.__last_subscr_time < 0.1: 378 | time.sleep(0.1) 379 | 380 | subscr_packet = Packet(self.ip) 381 | subscr_packet.compile(SUBSCRIBE, self.mac, SPACES_6, _reverse_bytes(self.mac), SPACES_6) 382 | subscr_packet.send(s) 383 | response = subscr_packet.recv_all(s, SUBSCRIBE_RESP) 384 | 385 | self.__last_subscr_time = time.time() 386 | return response.data[-1] if response is not None else None 387 | 388 | def __control_s20(self, switchOn): 389 | """ Switch S20 wifi socket on/off 390 | 391 | Arguments: 392 | switchOn -- True to switch on socket, False to switch off 393 | 394 | returns -- True if switch success, otherwise False 395 | """ 396 | 397 | with _orvibo_socket(self.__socket) as s: 398 | curr_state = self.__subscribe(s) 399 | 400 | if self.type != Orvibo.TYPE_SOCKET: 401 | self.__logger.warn('Attempt to control device with type {} as socket.'.format(self.type)) 402 | return False 403 | 404 | if curr_state is None: 405 | self.__logger.warn('Subscription failed while controlling wifi socket') 406 | return False 407 | 408 | state = ON if switchOn else OFF 409 | if curr_state == state: 410 | self.__logger.warn('No need to switch {0} device which is already switched {0}'.format('on' if switchOn else 'off')) 411 | return False 412 | 413 | self.__logger.debug('Socket is switching {}'.format('on' if switchOn else 'off')) 414 | on_off_packet = Packet(self.ip) 415 | on_off_packet.compile(CONTROL, self.mac, SPACES_6, ZEROS_4, state) 416 | on_off_packet.send(s) 417 | if on_off_packet.recv(s, CONTROL_RESP) is None: 418 | self.__logger.warn('Socket switching {} failed.'.format('on' if switchOn else 'off')) 419 | return False 420 | 421 | self.__logger.info('Socket is switched {} successfuly.'.format('on' if switchOn else 'off')) 422 | return True 423 | 424 | @property 425 | def on(self): 426 | """ State property for TYPE_SOCKET 427 | 428 | Arguments: 429 | returns -- State of device (True for on/False for off). 430 | """ 431 | 432 | onValue = 1 if py3 else ON 433 | return self.subscribe() == onValue 434 | 435 | @on.setter 436 | def on(self, state): 437 | """ Change device state for TYPE_SOCKET 438 | 439 | Arguments: 440 | state -- True (on) or False (off). 441 | 442 | returns -- nothing 443 | """ 444 | self.__control_s20(state) 445 | 446 | def learn_ir(self, fname = None, timeout = 15): 447 | """ Backward compatibility 448 | """ 449 | return self.learn(self, fname, timeout) 450 | 451 | def learn_rf433(self, fname = None): 452 | """ Learn Orvibo SmartSwitch RF433 signal. 453 | """ 454 | # It is actually the same packet as for RF433 signal emit. 455 | key = _random_n_bytes(7) 456 | 457 | if fname is not None: 458 | with open(fname, 'wb') as f: 459 | f.write(key) 460 | 461 | self._learn_emit_rf433(1, key) 462 | return key 463 | 464 | def learn(self, fname = None, timeout = 15): 465 | """ Read signal using your remote for future emit 466 | Supports IR and RF 433MHz remotes 467 | 468 | Arguments: 469 | fname -- [optional] file name to store IR/RF433 signal to 470 | timeout -- number of seconds to wait for IR/RF433 signal from remote 471 | 472 | returns -- byte string with IR/RD433 signal 473 | """ 474 | 475 | with _orvibo_socket(self.__socket) as s: 476 | if self.__subscribe(s) is None: 477 | self.__logger.warn('Subscription failed while entering to Learning IR/RF433 mode') 478 | return 479 | 480 | if self.type != Orvibo.TYPE_IRDA: 481 | self.__logger.warn('Attempt to enter to Learning IR/RF433 mode for device with type {}'.format(self.type)) 482 | return 483 | 484 | self.__logger.debug('Entering to Learning IR/RF433 mode') 485 | 486 | learn_packet = Packet(self.ip).compile(LEARN_IR, self.mac, SPACES_6, b'\x01\x00', ZEROS_4) 487 | learn_packet.send(s) 488 | if learn_packet.recv(s, LEARN_IR_RESP) is None: 489 | self.__logger.warn('Failed to enter to Learning IR/RF433 mode') 490 | return 491 | 492 | self.__logger.info('Waiting {} sec for IR/RF433 signal...'.format(timeout)) 493 | 494 | 495 | # LEARN_IR responses with such length will be skipped 496 | EMPTY_LEARN_IR = b'\x00\x18' 497 | 498 | start_time = time.time() 499 | while True: 500 | elapsed_time = time.time() - start_time 501 | if elapsed_time > timeout: 502 | self.__logger.warn('Nothing happend during {} sec'.format(timeout)) 503 | return 504 | 505 | packet_with_signal = learn_packet.recv(s, timeout=1) 506 | if packet_with_signal is None: 507 | self.__logger.info('The rest time: {} sec'.format(int(timeout - elapsed_time))) 508 | continue 509 | 510 | if packet_with_signal.length == EMPTY_LEARN_IR: 511 | self.__logger.debug('Skipped:\nEmpty packet = {}'.format(_debug_data(packet_with_signal.data))) 512 | continue 513 | 514 | if packet_with_signal.cmd == LEARN_IR: 515 | self.__logger.debug('SUCCESS:\n{}'.format(_debug_data(packet_with_signal.data))) 516 | break 517 | 518 | self.__logger.debug('Skipped:\nUnexpected packet = {}'.format(_debug_data(packet_with_signal.data))) 519 | 520 | signal_split = packet_with_signal.data.split(self.mac + SPACES_6, 1) 521 | signal = signal_split[1][6:] 522 | 523 | if fname is not None: 524 | with open(fname, 'wb') as f: 525 | f.write(signal) 526 | self.__logger.info('IR/RF433 signal got successfuly and saved to "{}" file'.format(fname)) 527 | else: 528 | self.__logger.info('IR/RF433 signal got successfuly') 529 | 530 | return signal 531 | 532 | def _learn_emit_rf433(self, on, key): 533 | """ Learn/emit SmartSwitch RF433 signal. 534 | """ 535 | with _orvibo_socket(self.__socket) as s: 536 | # this also comes with 64 62 packet 537 | signal_packet = Packet(self.ip).compile(BLAST_RF433, self.mac, SPACES_6, key[:4],\ 538 | _packet_id(), b'\x01' if on else b'\x00', b'\x29\x00', key[4:]) 539 | signal_packet.send(s) 540 | signal_packet.recv_all(s) 541 | self.__logger.debug('{}'.format(signal_packet)) 542 | 543 | def emit_rf433(self, on, fname): 544 | """ Emit RF433 signal for Orvibo SmartSwitch only. 545 | """ 546 | key = b'' 547 | with open(fname, 'rb') as f: 548 | key = f.read() 549 | 550 | self._learn_emit_rf433(on, key) 551 | 552 | 553 | def emit_ir(self, signal): 554 | """ Emit IR signal 555 | 556 | Arguments: 557 | signal -- raw signal got with learn method or file name with ir signal to emit 558 | 559 | returns -- True if emit successs, otherwise False 560 | """ 561 | 562 | with _orvibo_socket(self.__socket) as s: 563 | if self.__subscribe(s) is None: 564 | self.__logger.warn('Subscription failed while emiting IR signal') 565 | return False 566 | 567 | if self.type != Orvibo.TYPE_IRDA: 568 | self.__logger.warn('Attempt to emit IR signal for device with type {}'.format(self.type)) 569 | return False 570 | 571 | if isinstance(signal, str): 572 | # Read IR code from file 573 | self.__logger.debug('Reading IR signal from file "{}"'.format(signal)) 574 | with open(signal, 'rb') as f: 575 | signal = f.read() 576 | 577 | signal_packet = Packet(self.ip).compile(BLAST_IR, self.mac, SPACES_6, b'\x65\x00\x00\x00', _packet_id(), signal) 578 | signal_packet.send(s) 579 | signal_packet.recv_all(s) 580 | self.__logger.info('IR signal emit successfuly') 581 | return True 582 | 583 | def usage(): 584 | print('orvibo.py [-v] [-L ] [-i ] [-m -x ] [-s ] [-e ] [-t ] [-r]') 585 | print('-i - ip address of the Orvibo device, e.g 192.168.1.10') 586 | print('-m - mac address string, e.g acdf4377dfcc') 587 | print(' Not valid without -i and -x options') 588 | print('-x - type of the Orvibo device: socket, irda') 589 | print(' Not valid without -i and -m options') 590 | print('-s - switch on/off Orvibo Smart Socket: on, off') 591 | print(' Not valid without -i option and device types other than socket') 592 | print('-t - turns Orvibo AllOne into learning mode for 15 seconds or until catching IR signal') 593 | print(' Signal will be saved in "fname" file') 594 | print(' Not valid without -i option and device types other than "irda"') 595 | print('-e - emits IR signal stored in "fname" file') 596 | print(' Not valid without -i option or device types other than "irda"') 597 | print('-r - tells module to teach/emit RF433 signal for Orvibo SmartSwitch') 598 | print(' Not valid without -i option or device types other than "irda"') 599 | print('-v - prints module version') 600 | print('-L - extended output information: debug, info, warn') 601 | print() 602 | print('Examples:') 603 | print('Discover all Orvibo devices on the network:') 604 | print('> orvibo.py') 605 | print('Discover all Orvibo device by ip:') 606 | print('> orvibo.py -i 192.168.1.10') 607 | print('Discover all Orvibo known device. Much faster than previous one:') 608 | print('> orvibo.py -i 192.168.1.10 -m acdf4377dfcc -x socket') 609 | print('Switch socket on:') 610 | print('> orvibo.py -i 192.168.1.10 -m acdf4377dfcc -x socket -s on') 611 | print('Grab IR signal:') 612 | print('> orvibo.py -i 192.168.1.20 -m bdea54883ade -x irda -t signal.ir') 613 | print('Emit IR signal:') 614 | print('> orvibo.py -i 192.168.1.20 -m bdea54883ade -x irda -e signal.ir') 615 | print('Grab SmartSwitch RF signal:') 616 | print('> orvibo.py -i 192.168.1.20 -m bdea54883ade -x irda -t smartswitch.rf -r') 617 | print('Emit SmartSwitch RF signal:') 618 | print('> orvibo.py -i 192.168.1.20 -m bdea54883ade -x irda -e signal.ir -r -s on') 619 | 620 | if __name__ == '__main__': 621 | import sys 622 | import getopt 623 | 624 | class Opts: 625 | def __init__(self): 626 | self.help = False 627 | self.version = False 628 | self.log_level = logging.WARN 629 | self.ip = None 630 | self.mac = None 631 | self.otype = None 632 | self.switch = None 633 | self.emitFile = None 634 | self.teachFile = None 635 | self.rf = False 636 | 637 | def init(self): 638 | try: 639 | opts, args = getopt.getopt(sys.argv[1:], "rhvL:i:x:m:s:e:t:", ['loglevel=','ip=','mac=','type','socket=','emit=','teach=','zeach=']) 640 | except getopt.GetoptError: 641 | return False 642 | 643 | for opt, arg in opts: 644 | if opt == ('-h', '--help'): 645 | self.help = True 646 | elif opt in ('-v', '--version'): 647 | self.version = True 648 | elif opt in ('-L', '--loglevel'): 649 | if arg.lower() == 'debug': 650 | self.log_level = logging.DEBUG 651 | elif arg.lower() == 'info': 652 | self.log_level = logging.INFO 653 | elif arg.lower() == 'warn': 654 | self.log_level = logging.WARN 655 | elif opt in ('-i', '--ip'): 656 | self.ip = arg 657 | elif opt in ('-x', '--type'): 658 | self.otype = arg 659 | elif opt in ('-m', '--mac'): 660 | self.mac = arg 661 | elif opt in ('-s', '--socket'): 662 | self.switch = True if arg.lower() == 'on' or arg == '1' else False 663 | elif opt in ('-e', '--emit'): 664 | self.emitFile = arg 665 | elif opt in ('-t', '--teach'): 666 | self.teachFile = arg 667 | elif opt in ("-r", "--rf"): 668 | self.rf = True 669 | return True 670 | 671 | def discover_all(self): 672 | return self.ip is None and self.mac is None and self.switch is None and self.emitFile is None and self.teachFile is None 673 | 674 | def ip_skipped(self): 675 | return self.ip is None and self.mac is not None and self.otype is not None 676 | 677 | def teach_rf(self): 678 | return self.teachFile is not None and self.rf 679 | 680 | def emit_rf(self): 681 | return self.emitFile is not None and self.rf and self.switch is not None 682 | 683 | def emit_ir(self): 684 | return self.emitFile is not None and not self.rf 685 | 686 | def teach_ir(self): 687 | return self.teachFile is not None and not self.rf 688 | 689 | o = Opts() 690 | if not o.init(): 691 | usage() 692 | sys.exit(2) 693 | 694 | if o.help: 695 | usage() 696 | sys.exit() 697 | 698 | if o.version: 699 | print(__version__) 700 | sys.exit() 701 | sys.exit(0) 702 | 703 | logging.basicConfig(level=o.log_level) 704 | 705 | if o.discover_all(): 706 | for d in Orvibo.discover().values(): 707 | d = Orvibo(*d) 708 | print(d) 709 | sys.exit(0) 710 | 711 | if o.ip_skipped(): 712 | d = Orvibo(BROADCAST, o.mac, o.otype) 713 | elif o.mac is None: 714 | try: 715 | d = Orvibo.discover(o.ip) 716 | except OrviboException as e: 717 | print(e) 718 | sys.exit(-1) 719 | else: 720 | d = Orvibo(o.ip, o.mac, o.otype) 721 | 722 | print(d) 723 | 724 | if d.type == Orvibo.TYPE_SOCKET: 725 | if o.switch is None: 726 | print('Is enabled: {}'.format(d.on)) 727 | else: 728 | if d.on != o.switch: 729 | d.on = o.switch 730 | print('Is enabled: {}'.format(d.on)) 731 | else: 732 | print('Already {}.'.format('enabled' if o.switch else 'disabled')) 733 | elif d.type == Orvibo.TYPE_IRDA: 734 | if o.emit_rf(): 735 | # It is required to wake up AllOne 736 | d.emit_ir(b' ') 737 | d.emit_rf433(o.switch, o.emitFile) 738 | print('Emit RF done.') 739 | elif o.emit_ir(): 740 | d.emit_ir(o.emitFile) 741 | print('Emit IR done.') 742 | elif o.teach_rf(): 743 | # It is required to wake up AllOne 744 | d.emit_ir(b' ') 745 | signal = d.learn_rf433(o.teachFile) 746 | print('Teach RF done.') 747 | elif o.teach_ir(): 748 | signal = d.learn(o.teachFile) 749 | print('Teach IR done') 750 | --------------------------------------------------------------------------------