diff --git a/LICENSE.txt b/LICENSE.txt index c1c093a..812fb5e 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Anton Morozenko +Copyright (c) 2019-2020 Anton Morozenko Copyright (c) 2015-2019 Volodymyr Shymanskyy Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 7515d83..b047946 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +> [!IMPORTANT] +> **This project is still available for exploration, but is no longer actively maintained or updated.** +> We recommend switching to the Blynk MQTT API for a robust and future-proof experience. +> Support for this project will be phased out over time. +> You can explore some [useful MQTT examples here](https://github.com/Blynk-Technologies/Blynk-MQTT-Samples). + # Blynk Python Library This library provides API to connect IoT hardware that supports Micropython/Python to Blynk Cloud and communiate with Blynk apps (iOS and Android). You can send raw and processed sensor data and remotely control anything that is connected to your hardware (relays, motors, servos) from anywhere in the world. @@ -60,10 +66,12 @@ pip install --user -e . ``` #### Testing -You can run unit tests on cPython systems using the command: +You can run unit tests for cPython version of library (blynklib.py) using the command: python setup.py test +**NOTE** Blynklib version <0.2.6 should use pytest-mock<1.11.2. In version 1.11.2 were added restrictions for context manager usage + **NOTE:** Unit tests for Micropython ENV are not available yet. #### Micropython installation @@ -73,14 +81,14 @@ and related installation docs can be found [here][micropython-pkg]. ## Features -This library supports Python2, Python3, and Micropython. +This library supports Python2, Python3 (blynklib.py) , and Micropython (blynklib_mp.py). - Communication with public or local [Blynk Server][blynk-server]. - Exchange any data between your hardware and app - Tested to work with: Raspberry Pi (any), ESP32, ESP8266 ##### List of available operations: - - Subscribe to connect/disconnect events + - Subscribe to connect/disconnect events (ssl connection supported only by cPython lib) - Subscribe to read/write events of [virtual pins][blynk-vpins] - [Virtual Pin][blynk-vpins] write - [Virtual Pin][blynk-vpins] sync @@ -108,6 +116,7 @@ BLYNK_AUTH = '' #insert your Auth Token here #### Usage example ```python import blynklib +# import blynklib_mp as blynklib # micropython import BLYNK_AUTH = '' #insert your Auth Token here # base lib init @@ -115,7 +124,14 @@ blynk = blynklib.Blynk(BLYNK_AUTH) # advanced options of lib init # from __future__ import print_function -# blynk = blynklib.Blynk(BLYNK_AUTH, server='blynk-cloud.com', port=80, heartbeat=10, rcv_buffer=1024, log=print) +# blynk = blynklib.Blynk(BLYNK_AUTH, server='blynk-cloud.com', port=80, ssl_cert=None, +# heartbeat=10, rcv_buffer=1024, log=print) + +# Lib init with SSL socket connection +# blynk = blynklib.Blynk(BLYNK_AUTH, port=443, ssl_cert='') +# current blynk-cloud.com certificate stored in project as +# https://github.com/blynkkk/lib-python/blob/master/certificate/blynk-cloud.com.crt +# Note! ssl feature supported only by cPython # register handler for Virtual Pin V22 reading by Blynk App. # when a widget in Blynk App asks Virtual Pin data from server within given configurable interval (1,2,5,10 sec etc) @@ -160,7 +176,11 @@ Examples can be found **[here][blynk-py-examples]** Check them all to get famili - [06_terminal_widget.py](https://github.com/blynkkk/lib-python/blob/master/examples/06_terminal_widget.py): Communication between hardware and app through Terminal widget) - [07_tweet_and_logging.py](https://github.com/blynkkk/lib-python/blob/master/examples/07_tweet_and_logging.py): How to post to Twitter and log events from your hardware - [08_blynk_timer.py](https://github.com/blynkkk/lib-python/blob/master/examples/08_blynk_timer.py): How send data periodically from hardware by using **[Blynk Timer][blynktimer-doc]** -- [09_sync_virtual_pin.py](https://github.com/blynkkk/lib-python/blob/master/examples/09_sync_virtual_pin.py): How to sync virtual pin states and properties +- [09_sync_virtual_pin.py](https://github.com/blynkkk/lib-python/blob/master/examples/09_sync_virtual_pin.py): How to sync virtual pin states and properties +- [10_rtc_sync.py](https://github.com/blynkkk/lib-python/blob/master/examples/10_rtc_sync.py): How to perform RTC sync with blynk server +- [11_ssl_socket.py](https://github.com/blynkkk/lib-python/blob/master/examples/11_ssl_socket.py): SSL server connection. Feature supported only by cPython library. +- [12_app_connect_disconnect.py](https://github.com/blynkkk/lib-python/blob/master/examples/12_app_connect_disconnect.py): Managing APP connect/disconnect events with Blynk Cloud. + ##### Raspberry Pi (any): Read [Raspberry Pi guide](https://github.com/blynkkk/lib-python/tree/master/examples/raspberry) first. @@ -187,13 +207,9 @@ Read [this document][esp8266-readme] to get more information. ## Documentation and other helpful links -[Full Blynk Documentation](http://docs.blynk.cc/#blynk-firmware) - a complete guide on Blynk features - -[Community (Forum)](http://community.blynk.cc) - join a 500,000 Blynk community to ask questions and share ideas - -[Help Center](http://help.blynk.cc) - helpful articles on various Blynk aspects +[Full Blynk Documentation](https://docs.blynk.io) - a complete guide on Blynk features -[Code Examples Browser](http://examples.blynk.cc) - browse examples to explore Blynk possibilities +[Community (Forum)](https://community.blynk.cc) - join a 1'000'000 Blynk community to ask questions and share ideas [Official Website](https://blynk.io) @@ -212,7 +228,7 @@ Read [this document][esp8266-readme] to get more information. * [Lua, OpenWrt, NodeMCU](https://github.com/vshymanskyy/blynk-library-lua) * [OpenWrt packages](https://github.com/vshymanskyy/blynk-library-openwrt) * [MBED](https://developer.mbed.org/users/vshymanskyy/code/Blynk/) -* [Node-RED](https://www.npmjs.com/package/node-red-contrib-blynk-ws) +* [Node-RED for Blynk IoT](https://flows.nodered.org/node/node-red-contrib-blynk-iot) * [LabVIEW](https://github.com/juncaofish/NI-LabVIEWInterfaceforBlynk) * [C#](https://github.com/sverrefroy/BlynkLibrary) @@ -238,8 +254,8 @@ This project is released under The MIT License (MIT) [blynk-server-public]: http://blynk-cloud.com [blynk-docs]: https://docs.blynk.cc/ [blynk-py-examples]: https://github.com/blynkkk/lib-python/blob/master/examples - [blynk-app-android]: https://play.google.com/store/apps/details?id=cc.blynk - [blynk-app-ios]: https://itunes.apple.com/us/app/blynk-control-arduino-raspberry/id808760481?ls=1&mt=8 + [blynk-app-android]: https://play.google.com/store/apps/details?id=cloud.blynk + [blynk-app-ios]: https://apps.apple.com/us/app/blynk-iot/id1559317868 [blynk-vpins]: http://help.blynk.cc/getting-started-library-auth-token-code-examples/blynk-basics/what-is-virtual-pins [python-org]: https://www.python.org/downloads/ [micropython-org]: https://micropython.org/ diff --git a/blynklib.py b/blynklib.py index 942a1c8..1f15a30 100644 --- a/blynklib.py +++ b/blynklib.py @@ -1,29 +1,13 @@ -# Copyright (c) 2019 Anton Morozenko +# Copyright (c) 2019-2020 Anton Morozenko # Copyright (c) 2015-2019 Volodymyr Shymanskyy. # See the file LICENSE for copying permission. -__version__ = '0.2.5' +__version__ = '0.2.6' -try: - import usocket as socket - import utime as time - import ustruct as struct - import uselect as select - from micropython import const - - ticks_ms = time.ticks_ms - sleep_ms = time.sleep_ms - - IOError = OSError -except ImportError: - import socket - import time - import struct - import select - - const = lambda x: x - ticks_ms = lambda: int(time.time() * 1000) - sleep_ms = lambda x: time.sleep(x // 1000) +import socket +import ssl +import struct +import time LOGO = """ ___ __ __ @@ -37,6 +21,14 @@ def stub_log(*args): pass +def ticks_ms(): + return int(time.time() * 1000) + + +def sleep_ms(ms): + time.sleep(ms // 1000) + + class BlynkError(Exception): pass @@ -48,26 +40,26 @@ def __init__(self, server, port): class Protocol(object): - MSG_RSP = const(0) - MSG_LOGIN = const(2) - MSG_PING = const(6) - MSG_TWEET = const(12) - MSG_EMAIL = const(13) - MSG_NOTIFY = const(14) - MSG_BRIDGE = const(15) - MSG_HW_SYNC = const(16) - MSG_INTERNAL = const(17) - MSG_PROPERTY = const(19) - MSG_HW = const(20) - MSG_REDIRECT = const(41) - MSG_HEAD_LEN = const(5) - - STATUS_INVALID_TOKEN = const(9) - STATUS_NO_DATA = const(17) - STATUS_OK = const(200) - VPIN_MAX_NUM = const(32) - - _msg_id = 1 + MSG_RSP = 0 + MSG_LOGIN = 2 + MSG_PING = 6 + MSG_TWEET = 12 + MSG_EMAIL = 13 + MSG_NOTIFY = 14 + MSG_BRIDGE = 15 + MSG_HW_SYNC = 16 + MSG_INTERNAL = 17 + MSG_PROPERTY = 19 + MSG_HW = 20 + MSG_REDIRECT = 41 + MSG_HEAD_LEN = 5 + + STATUS_INVALID_TOKEN = 9 + STATUS_NO_DATA = 17 + STATUS_OK = 200 + VPIN_MAX_NUM = 32 + + _msg_id = 0 def _get_msg_id(self, **kwargs): if 'msg_id' in kwargs: @@ -89,9 +81,9 @@ def parse_response(self, rsp_data, msg_buffer): raise BlynkError('invalid msg_id == 0') elif h_data >= msg_buffer: raise BlynkError('Command too long. Length = {}'.format(h_data)) - elif msg_type in (self.MSG_RSP, self.MSG_PING, self.MSG_INTERNAL): + elif msg_type in (self.MSG_RSP, self.MSG_PING): pass - elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_REDIRECT): + elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_INTERNAL, self.MSG_REDIRECT): msg_body = rsp_data[self.MSG_HEAD_LEN: self.MSG_HEAD_LEN + h_data] msg_args = [itm.decode('utf-8') for itm in msg_body.split(b'\0')] else: @@ -129,20 +121,24 @@ def notify_msg(self, msg): def set_property_msg(self, pin, prop, *val): return self._pack_msg(self.MSG_PROPERTY, pin, prop, *val) + def internal_msg(self, *args): + return self._pack_msg(self.MSG_INTERNAL, *args) + class Connection(Protocol): - SOCK_MAX_TIMEOUT = const(5) + SOCK_MAX_TIMEOUT = 5 SOCK_TIMEOUT = 0.05 - EAGAIN = const(11) - ETIMEDOUT = const(60) - RETRIES_TX_DELAY = const(2) - RETRIES_TX_MAX_NUM = const(3) - RECONNECT_SLEEP = const(1) - TASK_PERIOD_RES = const(50) - DISCONNECTED = const(0) - CONNECTING = const(1) - AUTHENTICATING = const(2) - AUTHENTICATED = const(3) + SOCK_SSL_TIMEOUT = 1 + EAGAIN = 11 + ETIMEDOUT = 60 + RETRIES_TX_DELAY = 2 + RETRIES_TX_MAX_NUM = 3 + RECONNECT_SLEEP = 1 + TASK_PERIOD_RES = 50 + DISCONNECTED = 0 + CONNECTING = 1 + AUTHENTICATING = 2 + AUTHENTICATED = 3 _state = None _socket = None @@ -150,21 +146,15 @@ class Connection(Protocol): _last_ping_time = 0 _last_send_time = 0 - def __init__(self, token, server='blynk-cloud.com', port=80, heartbeat=10, rcv_buffer=1024, log=stub_log): + def __init__(self, token, server='blynk-cloud.com', port=80, ssl_cert=None, heartbeat=10, rcv_buffer=1024, + log=stub_log): self.token = token self.server = server self.port = port self.heartbeat = heartbeat self.rcv_buffer = rcv_buffer self.log = log - - def _set_socket_timeout(self, timeout): - if getattr(self._socket, 'settimeout', None): - self._socket.settimeout(timeout) - else: - p = select.poll() - p.register(self._socket) - p.poll(int(timeout * 1000)) + self.ssl_cert = ssl_cert def send(self, data): retries = self.RETRIES_TX_MAX_NUM @@ -179,13 +169,13 @@ def send(self, data): def receive(self, length, timeout): d_buff = b'' try: - self._set_socket_timeout(timeout) + self._socket.settimeout(timeout) d_buff += self._socket.recv(length) if len(d_buff) >= length: d_buff = d_buff[:length] return d_buff except (IOError, OSError) as err: - if str(err) == 'timed out': + if 'timed out' in str(err): return b'' if str(self.EAGAIN) in str(err) or str(self.ETIMEDOUT) in str(err): return b'' @@ -210,7 +200,16 @@ def _get_socket(self): self._state = self.CONNECTING self._socket = socket.socket() self._socket.connect(socket.getaddrinfo(self.server, self.port)[0][4]) - self._set_socket_timeout(self.SOCK_TIMEOUT) + self._socket.settimeout(self.SOCK_TIMEOUT) + if self.ssl_cert: + # system default CA certificates case + if self.ssl_cert == "default": + self.ssl_cert = None + self.log('Using SSL socket...') + ssl_context = ssl.create_default_context(cafile=self.ssl_cert) + ssl_context.verify_mode = ssl.CERT_REQUIRED + self._socket.settimeout(self.SOCK_SSL_TIMEOUT) + self._socket = ssl_context.wrap_socket(sock=self._socket, server_hostname=self.server) self.log('Connected to blynk server') except Exception as g_exc: raise BlynkError('Connection with the Blynk server failed: {}'.format(g_exc)) @@ -247,7 +246,7 @@ def connected(self): class Blynk(Connection): - _CONNECT_TIMEOUT = const(30) # 30sec + _CONNECT_TIMEOUT = 30 # 30sec _VPIN_WILDCARD = '*' _VPIN_READ = 'read v' _VPIN_WRITE = 'write v' @@ -275,6 +274,7 @@ def connect(self, timeout=_CONNECT_TIMEOUT): self._get_socket() self._authenticate() self._set_heartbeat() + self._last_rcv_time = ticks_ms() self.log('Registered events: {}\n'.format(list(self._events.keys()))) self.call_handler(self._CONNECT) return True @@ -296,6 +296,7 @@ def disconnect(self, err_msg=None): self._state = self.DISCONNECTED if err_msg: self.log('[ERROR]: {}\nConnection closed'.format(err_msg)) + self._msg_id = 0 time.sleep(self.RECONNECT_SLEEP) def virtual_write(self, v_pin, *val): @@ -316,6 +317,9 @@ def notify(self, msg): def set_property(self, v_pin, property_name, *val): return self.send(self.set_property_msg(v_pin, property_name, *val)) + def internal(self, *args): + return self.send(self.internal_msg(*args)) + def handle_event(blynk, event_name): class Deco(object): def __init__(self, func): @@ -344,19 +348,19 @@ def process(self, msg_type, msg_id, msg_len, msg_args): elif msg_type == self.MSG_PING: self.send(self.response_msg(self.STATUS_OK, msg_id=msg_id)) elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_INTERNAL): - if msg_type == self.MSG_INTERNAL and len(msg_args) >= const(3): - self.call_handler("{}{}".format(self._INTERNAL, msg_args[1]), msg_args[2:]) - elif len(msg_args) >= const(3) and msg_args[0] == 'vw': + if msg_type == self.MSG_INTERNAL: + self.call_handler("{}{}".format(self._INTERNAL, msg_args[0]), msg_args[1:]) + elif len(msg_args) >= 3 and msg_args[0] == 'vw': self.call_handler("{}{}".format(self._VPIN_WRITE, msg_args[1]), int(msg_args[1]), msg_args[2:]) - elif len(msg_args) == const(2) and msg_args[0] == 'vr': + elif len(msg_args) == 2 and msg_args[0] == 'vr': self.call_handler("{}{}".format(self._VPIN_READ, msg_args[1]), int(msg_args[1])) def read_response(self, timeout=0.5): end_time = time.time() + timeout while time.time() <= end_time: rsp_data = self.receive(self.rcv_buffer, self.SOCK_TIMEOUT) - self._last_rcv_time = ticks_ms() if rsp_data: + self._last_rcv_time = ticks_ms() msg_type, msg_id, h_data, msg_args = self.parse_response(rsp_data, self.rcv_buffer) self.process(msg_type, msg_id, h_data, msg_args) diff --git a/blynklib_mp.py b/blynklib_mp.py new file mode 100644 index 0000000..46f8897 --- /dev/null +++ b/blynklib_mp.py @@ -0,0 +1,377 @@ +# Copyright (c) 2019-2020 Anton Morozenko +# Copyright (c) 2015-2019 Volodymyr Shymanskyy. +# See the file LICENSE for copying permission. + +__version__ = '0.2.6' + +import usocket as socket +import utime as time +import ustruct as struct +import uselect as select +from micropython import const + +ticks_ms = time.ticks_ms +sleep_ms = time.sleep_ms + +IOError = OSError + +LOGO = """ + ___ __ __ + / _ )/ /_ _____ / /__ + / _ / / // / _ \\/ '_/ + /____/_/\\_, /_//_/_/\\_\\ + /___/ for Python v{}\n""".format(__version__) + + +def stub_log(*args): + pass + + +class BlynkError(Exception): + pass + + +class RedirectError(Exception): + def __init__(self, server, port): + self.server = server + self.port = port + + +class Protocol(object): + MSG_RSP = const(0) + MSG_LOGIN = const(2) + MSG_PING = const(6) + MSG_TWEET = const(12) + MSG_EMAIL = const(13) + MSG_NOTIFY = const(14) + MSG_BRIDGE = const(15) + MSG_HW_SYNC = const(16) + MSG_INTERNAL = const(17) + MSG_PROPERTY = const(19) + MSG_HW = const(20) + MSG_REDIRECT = const(41) + MSG_HEAD_LEN = const(5) + + STATUS_INVALID_TOKEN = const(9) + STATUS_OK = const(200) + VPIN_MAX_NUM = const(32) + + _msg_id = 1 + + def _get_msg_id(self, **kwargs): + if 'msg_id' in kwargs: + return kwargs['msg_id'] + self._msg_id += const(1) + return self._msg_id if self._msg_id <= const(0xFFFF) else const(1) + + def _pack_msg(self, msg_type, *args, **kwargs): + data = ('\0'.join([str(curr_arg) for curr_arg in args])).encode('utf-8') + return struct.pack('!BHH', msg_type, self._get_msg_id(**kwargs), len(data)) + data + + def parse_response(self, rsp_data, msg_buffer): + msg_args = [] + msg_len = 0 + try: + msg_type, msg_id, h_data = struct.unpack('!BHH', rsp_data[:self.MSG_HEAD_LEN]) + msg_len = self.MSG_HEAD_LEN + h_data + except Exception as p_err: + raise BlynkError('Message parse error: {}'.format(p_err)) + if msg_id == 0: + raise BlynkError('invalid msg_id == 0') + elif h_data >= msg_buffer: + raise BlynkError('Command too long. Length = {}'.format(h_data)) + elif msg_type in (self.MSG_RSP, self.MSG_PING): + pass + elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_INTERNAL, self.MSG_REDIRECT): + msg_body = rsp_data[self.MSG_HEAD_LEN: msg_len] + msg_args = [itm.decode('utf-8') for itm in msg_body.split(b'\0')] + else: + raise BlynkError("Unknown message type: '{}'".format(msg_type)) + return msg_type, msg_id, h_data, msg_args, msg_len + + def heartbeat_msg(self, heartbeat, rcv_buffer): + return self._pack_msg(self.MSG_INTERNAL, 'ver', __version__, 'buff-in', rcv_buffer, 'h-beat', heartbeat, + 'dev', 'mpython') + + def login_msg(self, token): + return self._pack_msg(self.MSG_LOGIN, token) + + def ping_msg(self): + return self._pack_msg(self.MSG_PING) + + def response_msg(self, *args, **kwargs): + return self._pack_msg(self.MSG_RSP, *args, **kwargs) + + def virtual_write_msg(self, v_pin, *val): + return self._pack_msg(self.MSG_HW, 'vw', v_pin, *val) + + def virtual_sync_msg(self, *pins): + return self._pack_msg(self.MSG_HW_SYNC, 'vr', *pins) + + def email_msg(self, to, subject, body): + return self._pack_msg(self.MSG_EMAIL, to, subject, body) + + def tweet_msg(self, msg): + return self._pack_msg(self.MSG_TWEET, msg) + + def notify_msg(self, msg): + return self._pack_msg(self.MSG_NOTIFY, msg) + + def set_property_msg(self, pin, prop, *val): + return self._pack_msg(self.MSG_PROPERTY, pin, prop, *val) + + def internal_msg(self, *args): + return self._pack_msg(self.MSG_INTERNAL, *args) + + +class Connection(Protocol): + SOCK_MAX_TIMEOUT = const(5) + SOCK_TIMEOUT = 0.05 + EAGAIN = const(11) + ETIMEDOUT = const(60) + RETRIES_TX_DELAY = const(2) + RETRIES_TX_MAX_NUM = const(3) + RECONNECT_SLEEP = const(1) + TASK_PERIOD_RES = const(50) + DISCONNECTED = const(0) + CONNECTING = const(1) + AUTHENTICATING = const(2) + AUTHENTICATED = const(3) + + _state = None + _socket = None + _last_rcv_time = 0 + _last_ping_time = 0 + _last_send_time = 0 + + def __init__(self, token, server='blynk-cloud.com', port=80, heartbeat=10, rcv_buffer=1024, log=stub_log): + self.token = token + self.server = server + self.port = port + self.heartbeat = heartbeat + self.rcv_buffer = rcv_buffer + self.log = log + + def _set_socket_timeout(self, timeout): + if getattr(self._socket, 'settimeout', None): + self._socket.settimeout(timeout) + else: + p = select.poll() + p.register(self._socket) + p.poll(int(timeout * const(1000))) + + def send(self, data): + retries = self.RETRIES_TX_MAX_NUM + while retries > 0: + try: + retries -= 1 + self._last_send_time = ticks_ms() + return self._socket.send(data) + except (IOError, OSError): + sleep_ms(self.RETRIES_TX_DELAY) + + def receive(self, length, timeout): + d_buff = b'' + try: + self._set_socket_timeout(timeout) + d_buff += self._socket.recv(length) + if len(d_buff) >= length: + d_buff = d_buff[:length] + return d_buff + except (IOError, OSError) as err: + if str(err) == 'timed out': + return b'' + if str(self.EAGAIN) in str(err) or str(self.ETIMEDOUT) in str(err): + return b'' + raise + + def is_server_alive(self): + now = ticks_ms() + h_beat_ms = self.heartbeat * const(1000) + rcv_delta = time.ticks_diff(now, self._last_rcv_time) + ping_delta = time.ticks_diff(now, self._last_ping_time) + send_delta = time.ticks_diff(now, self._last_send_time) + if rcv_delta > h_beat_ms + (h_beat_ms // const(2)): + return False + if (ping_delta > h_beat_ms // const(10)) and (send_delta > h_beat_ms or rcv_delta > h_beat_ms): + self.send(self.ping_msg()) + self.log('Heartbeat time: {}'.format(now)) + self._last_ping_time = now + return True + + def _get_socket(self): + try: + self._state = self.CONNECTING + self._socket = socket.socket() + self._socket.connect(socket.getaddrinfo(self.server, self.port)[0][-1]) + self._set_socket_timeout(self.SOCK_TIMEOUT) + self.log('Connected to server') + except Exception as g_exc: + raise BlynkError('Server connection failed: {}'.format(g_exc)) + + def _authenticate(self): + self.log('Authenticating device...') + self._state = self.AUTHENTICATING + self.send(self.login_msg(self.token)) + rsp_data = self.receive(self.rcv_buffer, self.SOCK_MAX_TIMEOUT) + if not rsp_data: + raise BlynkError('Auth stage timeout') + msg_type, _, status, args, _ = self.parse_response(rsp_data, self.rcv_buffer) + if status != self.STATUS_OK: + if status == self.STATUS_INVALID_TOKEN: + raise BlynkError('Invalid Auth Token') + if msg_type == self.MSG_REDIRECT: + raise RedirectError(*args) + raise BlynkError('Auth stage failed. Status={}'.format(status)) + self._state = self.AUTHENTICATED + self.log('Access granted') + + def _set_heartbeat(self): + self.send(self.heartbeat_msg(self.heartbeat, self.rcv_buffer)) + rcv_data = self.receive(self.rcv_buffer, self.SOCK_MAX_TIMEOUT) + if not rcv_data: + raise BlynkError('Heartbeat stage timeout') + _, _, status, _, _ = self.parse_response(rcv_data, self.rcv_buffer) + if status != self.STATUS_OK: + raise BlynkError('Set heartbeat returned code={}'.format(status)) + self.log('Heartbeat = {} sec. MaxCmdBuffer = {} bytes'.format(self.heartbeat, self.rcv_buffer)) + + def connected(self): + return True if self._state == self.AUTHENTICATED else False + + +class Blynk(Connection): + _CONNECT_TIMEOUT = const(30) # 30sec + _VPIN_WILDCARD = '*' + _VPIN_READ = 'read v' + _VPIN_WRITE = 'write v' + _INTERNAL = 'internal_' + _CONNECT = 'connect' + _DISCONNECT = 'disconnect' + _VPIN_READ_ALL = '{}{}'.format(_VPIN_READ, _VPIN_WILDCARD) + _VPIN_WRITE_ALL = '{}{}'.format(_VPIN_WRITE, _VPIN_WILDCARD) + _events = {} + + def __init__(self, token, **kwargs): + Connection.__init__(self, token, **kwargs) + self._start_time = ticks_ms() + self._last_rcv_time = ticks_ms() + self._last_send_time = ticks_ms() + self._last_ping_time = ticks_ms() + self._state = self.DISCONNECTED + print(LOGO) + + def connect(self, timeout=_CONNECT_TIMEOUT): + end_time = time.time() + timeout + while not self.connected(): + if self._state == self.DISCONNECTED: + try: + self._get_socket() + self._authenticate() + self._set_heartbeat() + self._last_rcv_time = ticks_ms() + self.log('Registered events: {}\n'.format(list(self._events.keys()))) + self.call_handler(self._CONNECT) + return True + except BlynkError as b_err: + self.disconnect(b_err) + sleep_ms(self.TASK_PERIOD_RES) + except RedirectError as r_err: + self.disconnect() + self.server = r_err.server + self.port = r_err.port + sleep_ms(self.TASK_PERIOD_RES) + if time.time() >= end_time: + return False + + def disconnect(self, err_msg=None): + self.call_handler(self._DISCONNECT) + if self._socket: + self._socket.close() + self._state = self.DISCONNECTED + if err_msg: + self.log('[ERROR]: {}\nConnection closed'.format(err_msg)) + time.sleep(self.RECONNECT_SLEEP) + + def virtual_write(self, v_pin, *val): + return self.send(self.virtual_write_msg(v_pin, *val)) + + def virtual_sync(self, *v_pin): + return self.send(self.virtual_sync_msg(*v_pin)) + + def email(self, to, subject, body): + return self.send(self.email_msg(to, subject, body)) + + def tweet(self, msg): + return self.send(self.tweet_msg(msg)) + + def notify(self, msg): + return self.send(self.notify_msg(msg)) + + def set_property(self, v_pin, property_name, *val): + return self.send(self.set_property_msg(v_pin, property_name, *val)) + + def internal(self, *args): + return self.send(self.internal_msg(*args)) + + def handle_event(blynk, event_name): + class Deco(object): + def __init__(self, func): + self.func = func + # wildcard 'read V*' and 'write V*' events handling + if str(event_name).lower() in (blynk._VPIN_READ_ALL, blynk._VPIN_WRITE_ALL): + event_base_name = str(event_name).split(blynk._VPIN_WILDCARD)[0] + for i in range(blynk.VPIN_MAX_NUM + 1): + blynk._events['{}{}'.format(event_base_name.lower(), i)] = func + else: + blynk._events[str(event_name).lower()] = func + + def __call__(self): + return self.func() + + return Deco + + def call_handler(self, event, *args, **kwargs): + if event in self._events.keys(): + self.log("Event: ['{}'] -> {}".format(event, args)) + self._events[event](*args, **kwargs) + + def process(self, msg_type, msg_id, msg_len, msg_args): + if msg_type == self.MSG_RSP: + self.log('Response status: {}'.format(msg_len)) + elif msg_type == self.MSG_PING: + self.send(self.response_msg(self.STATUS_OK, msg_id=msg_id)) + elif msg_type in (self.MSG_HW, self.MSG_BRIDGE, self.MSG_INTERNAL): + if msg_type == self.MSG_INTERNAL: + self.call_handler("{}{}".format(self._INTERNAL, msg_args[0]), msg_args[1:]) + elif len(msg_args) >= const(3) and msg_args[0] == 'vw': + self.call_handler("{}{}".format(self._VPIN_WRITE, msg_args[1]), int(msg_args[1]), msg_args[2:]) + elif len(msg_args) == const(2) and msg_args[0] == 'vr': + self.call_handler("{}{}".format(self._VPIN_READ, msg_args[1]), int(msg_args[1])) + + def read_response(self, timeout=0.5): + end_time = time.ticks_ms() + int(timeout * const(1000)) + while time.ticks_diff(end_time, time.ticks_ms()) > 0: + rsp_data = self.receive(self.rcv_buffer, self.SOCK_TIMEOUT) + if rsp_data: + self._last_rcv_time = ticks_ms() + while rsp_data: + msg_type, msg_id, h_data, msg_args, msg_len = self.parse_response(rsp_data, self.rcv_buffer) + self.process(msg_type, msg_id, h_data, msg_args) + rsp_data = rsp_data[msg_len:] + + def run(self): + if not self.connected(): + self.connect() + else: + try: + self.read_response(timeout=self.SOCK_TIMEOUT) + if not self.is_server_alive(): + self.disconnect('Server is offline') + except KeyboardInterrupt: + raise + except BlynkError as b_err: + self.log(b_err) + self.disconnect() + except Exception as g_exc: + self.log(g_exc) diff --git a/blynktimer.py b/blynktimer.py index e8c0d9d..704a391 100644 --- a/blynktimer.py +++ b/blynktimer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 Anton Morozenko +# Copyright (c) 2019-2020 Anton Morozenko """ Polling timers for functions. Registers timers and performs run once or periodical function execution after defined time intervals. diff --git a/certificate/blynk-cloud.com.crt b/certificate/blynk-cloud.com.crt new file mode 100644 index 0000000..040d61d --- /dev/null +++ b/certificate/blynk-cloud.com.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID5TCCAs2gAwIBAgIJAIHSnb+cv4ECMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYD +VQQGEwJVQTENMAsGA1UECAwES3lpdjENMAsGA1UEBwwES3lpdjELMAkGA1UECgwC +SVQxEzARBgNVBAsMCkJseW5rIEluYy4xGDAWBgNVBAMMD2JseW5rLWNsb3VkLmNv +bTEfMB0GCSqGSIb3DQEJARYQZG1pdHJpeUBibHluay5jYzAeFw0xNjAzMTcxMTU4 +MDdaFw0yMTAzMTYxMTU4MDdaMIGIMQswCQYDVQQGEwJVQTENMAsGA1UECAwES3lp +djENMAsGA1UEBwwES3lpdjELMAkGA1UECgwCSVQxEzARBgNVBAsMCkJseW5rIElu +Yy4xGDAWBgNVBAMMD2JseW5rLWNsb3VkLmNvbTEfMB0GCSqGSIb3DQEJARYQZG1p +dHJpeUBibHluay5jYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALso +bhbXQuNlzYBFa9h9pd69n43yrGTL4Ba6k5Q1zDwY9HQbMdfC5ZfnCkqT7Zf+R5MO +RW0Q9nLsFNLJkwKnluRCYGyUES8NAmDLQBbZoVc8mv9K3mIgAQvGyY2LmKak5GSI +V0PC3x+iN03xU2774+Zi7DaQd7vTl/9RGk8McyHe/s5Ikbe14bzWcY9ZV4PKgCck +p1chbmLhSfGbT3v64sL8ZbIppQk57/JgsZMrVpjExvxQPZuJfWbtoypPfpYO+O8l +1szaMlTEPIZVMoYi9uE+DnOlhzJFn6Ac4FMrDzJXzMmCweSX3IxguvXALeKhUHQJ ++VP3G6Q3pkZRVKz+5XsCAwEAAaNQME4wHQYDVR0OBBYEFJtqtI62Io66cZgiTR5L +A5Tl5m+xMB8GA1UdIwQYMBaAFJtqtI62Io66cZgiTR5LA5Tl5m+xMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKphjtEOGs7oC3S87+AUgIw4gFNOuv+L +C98/l47OD6WtsqJKvCZ1lmKxY5aIro9FBPk8ktCOsbwEjE+nyr5wul+6CLFr+rnv +7OHYGwLpjoz+rZgYJiQ61E1m0AZ4y9Fyd+D90HW6247vrBXyEiUXOhN/oDDVfDQA +eqmNBx1OqWel81D3tA7zPMA7vUItyWcFIXNjOCP+POy7TMxZuhuPMh5bVu+/cthl +/Q9u/Z2lKl4CWV0Ivt2BtlN6iefva0e2AP/As+gfwjxrb0t11zSILLNJ+nxRIwg+ +k4MGb1zihKbIXUzsjslONK4FY5rlQUSwKJgEAVF0ClxB4g6dECm0ckc= +-----END CERTIFICATE----- diff --git a/examples/03_connect_disconnect.py b/examples/03_connect_disconnect.py index e15c813..da3154b 100644 --- a/examples/03_connect_disconnect.py +++ b/examples/03_connect_disconnect.py @@ -71,7 +71,7 @@ def connect_handler(): @blynk.handle_event("disconnect") -def connect_handler(): +def disconnect_handler(): print(DISCONNECT_PRINT_MSG) print('Sleeping 4 sec in disconnect handler...') time.sleep(4) diff --git a/examples/10_rtc_sync.py b/examples/10_rtc_sync.py new file mode 100644 index 0000000..f1a61bd --- /dev/null +++ b/examples/10_rtc_sync.py @@ -0,0 +1,77 @@ +""" +[RTC EXAMPLE] ======================================================================================================== + +Environment prepare: +In your Blynk App project: + - add "RTC" widget, + - set required TimeZone, + - Run the App (green triangle in the upper right corner). + - define your auth token for current example and run it + + +This started program on connect will send rtc_sync call to server. +RTC reply will be captured by "internal_rtc" handler +UTC time with required timezone correction will be printed + +Schema: +===================================================================================================================== + +-----------+ +--------------+ +--------------+ + | | | | | | + | blynk lib | | blynk server | | blynk app | + | | | | | | + | | | | | | + +-----+-----+ +------+-------+ +-------+------+ + | | | +connect handler | | | + +-------+ | | + | | | | + | | rtc sync | | + +------>------------------------------------->+ rtc widget present in app? | + | +----------------------------------->+ + | | | + | | yes rtc widget found | + | rtc with timezone correction +<-----------------------------------+ +internal_rtc | | | +handler +--------<------------------------------------+ | + | | | | + | | | | + +------>+ | | + | | | + | | | + + + + +===================================================================================================================== +Additional blynk info you can find by examining such resources: + + Downloads, docs, tutorials: https://blynk.io + Sketch generator: http://examples.blynk.cc + Blynk community: http://community.blynk.cc + Social networks: http://www.fb.com/blynkapp + http://twitter.com/blynk_app +===================================================================================================================== +""" + +import blynklib +from datetime import datetime + +BLYNK_AUTH = 'YourAuthToken' +blynk = blynklib.Blynk(BLYNK_AUTH) + + +@blynk.handle_event("connect") +def connect_handler(): + blynk.internal("rtc", "sync") + print("RTC sync request was sent") + + +@blynk.handle_event('internal_rtc') +def rtc_handler(rtc_data_list): + hr_rtc_value = datetime.utcfromtimestamp(int(rtc_data_list[0])).strftime('%Y-%m-%d %H:%M:%S') + print('Raw RTC value from server: {}'.format(rtc_data_list[0])) + print('Human readable RTC value: {}'.format(hr_rtc_value)) + + +########################################################### +# infinite loop that waits for event +########################################################### +while True: + blynk.run() diff --git a/examples/11_ssl_socket.py b/examples/11_ssl_socket.py new file mode 100644 index 0000000..273213b --- /dev/null +++ b/examples/11_ssl_socket.py @@ -0,0 +1,97 @@ +""" +[SSL CONNECT/DISCONNECT EVENTS EXAMPLE] ================================================================= +NOTE! + This example works correctly only fo cPython version of library (blynklib.py) + For micropython present limitation that keyword arguments of wrap_socket may be not supported by certain ports + + +Environment prepare: + - define your auth token for current example and run it + + +This started program after successful connect operation will call and execute "connect event handler" +Within handler after short sleep delay blynk disconnect call will be performed that will trigger +"disconnect event handler" execution. + +Schema: +===================================================================================================== + +-----------+ +--------------+ + | | | | + | blynk lib | | blynk server | + | | | virtual pin | + | | | | + +-----+-----+ +------+-------+ + | | + | connect/authenticate request | + +------------------------------------>+ + connect handler | | + (user function) | connected successfully | + +-----------<------------------------------------+ + | | | + | | disconnect request | + +--------->------------------------------------->+ + | | + | | +disconnect handler | | + (user function) | disconnected successfully | + +-----------<------------------------------------+ + | | | + | | | + +--------->+ | + | reconnect request | + | performed by lib automatically | + +------------------------------------>+ + + + + + +==================================================================================================== +Additional info about blynk you can find by examining such resources: + + Downloads, docs, tutorials: https://blynk.io + Sketch generator: http://examples.blynk.cc + Blynk community: http://community.blynk.cc + Social networks: http://www.fb.com/blynkapp + http://twitter.com/blynk_app +===================================================================================================== +""" + +import blynklib +import time +import logging + +# tune console logging +_log = logging.getLogger('BlynkLog') +logFormatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") +consoleHandler = logging.StreamHandler() +consoleHandler.setFormatter(logFormatter) +_log.addHandler(consoleHandler) +_log.setLevel(logging.DEBUG) + +BLYNK_AUTH = 'YourAuthToken' + +blynk = blynklib.Blynk(BLYNK_AUTH, port=443, ssl_cert='../certificate/blynk-cloud.com.crt', log=_log.info) + +CONNECT_PRINT_MSG = '[CONNECT_EVENT]' +DISCONNECT_PRINT_MSG = '[DISCONNECT_EVENT]' + + +@blynk.handle_event("connect") +def connect_handler(): + print(CONNECT_PRINT_MSG) + print('Sleeping 4 sec in SSL connect handler...') + time.sleep(4) + blynk.disconnect() + + +@blynk.handle_event("disconnect") +def disconnect_handler(): + print(DISCONNECT_PRINT_MSG) + print('Sleeping 3 sec in SSL disconnect handler...') + time.sleep(5) + + +########################################################### +# infinite loop that waits for event +########################################################### +while True: + blynk.run() diff --git a/examples/12_app_connect_disconnect.py b/examples/12_app_connect_disconnect.py new file mode 100644 index 0000000..2159466 --- /dev/null +++ b/examples/12_app_connect_disconnect.py @@ -0,0 +1,74 @@ +""" +[APP CONNECT DISCONNECT EVENTS EXAMPLE] ============================================================ + +Environment prepare: +In your Blynk App project: + - in Project Settings enable flag "Notify devices when APP connected" + - define your auth token for current example and run it + - Run the App (green triangle in the upper right corner). + +This started program will call handlers and print messages for APP_CONNECT or APP_DISCONNECT events. + +Schema: +==================================================================================================== + +-----------+ +--------------+ +--------------+ + | | | | | | + | blynk lib | | blynk server | | blynk app | + | | | virtual pin | | | + | | | | | | + +-----+-----+ +------+-------+ +-------+------+ + | | app connected or disconnected | + | | from server | + | | + event handler | app connect/disconnect event +<-----------------------------------+ +(user function) | | | + +------------<-----------------------------------+ | + | | | | + | | | | + +--------->+ | | + | | | + | | | + | | | + | | | + | | | + | | | + | | | + | | | + + + + +==================================================================================================== +Additional blynk info you can find by examining such resources: + + Downloads, docs, tutorials: https://blynk.io + Sketch generator: http://examples.blynk.cc + Blynk community: http://community.blynk.cc + Social networks: http://www.fb.com/blynkapp + http://twitter.com/blynk_app +==================================================================================================== +""" + +import blynklib + +BLYNK_AUTH = 'YourAuthToken' + +# initialize Blynk +blynk = blynklib.Blynk(BLYNK_AUTH) + +APP_CONNECT_PRINT_MSG = '[APP_CONNECT_EVENT]' +APP_DISCONNECT_PRINT_MSG = '[APP_DISCONNECT_EVENT]' + + +@blynk.handle_event('internal_acon') +def app_connect_handler(*args): + print(APP_CONNECT_PRINT_MSG) + + +@blynk.handle_event('internal_adis') +def app_disconnect_handler(*args): + print(APP_DISCONNECT_PRINT_MSG) + + +########################################################### +# infinite loop that waits for event +########################################################### +while True: + blynk.run() diff --git a/examples/esp32/01_touch_button.py b/examples/esp32/01_touch_button.py index 695dc79..c831a70 100644 --- a/examples/esp32/01_touch_button.py +++ b/examples/esp32/01_touch_button.py @@ -34,7 +34,7 @@ http://twitter.com/blynk_app ===================================================================================================================== """ -import blynklib +import blynklib_mp as blynklib import network import utime as time from machine import Pin diff --git a/examples/esp32/02_terminal_cli.py b/examples/esp32/02_terminal_cli.py index 126f926..9fa33a8 100644 --- a/examples/esp32/02_terminal_cli.py +++ b/examples/esp32/02_terminal_cli.py @@ -33,7 +33,7 @@ http://twitter.com/blynk_app ===================================================================================================================== """ -import blynklib +import blynklib_mp as blynklib import network import uos import utime as time diff --git a/examples/esp32/03_temperature_humidity_dht22.py b/examples/esp32/03_temperature_humidity_dht22.py index de8cdb5..87c3c0d 100644 --- a/examples/esp32/03_temperature_humidity_dht22.py +++ b/examples/esp32/03_temperature_humidity_dht22.py @@ -48,7 +48,7 @@ http://twitter.com/blynk_app ===================================================================================================================== """ -import blynklib +import blynklib_mp as blynklib import network import utime as time from machine import Pin diff --git a/examples/esp32/README.md b/examples/esp32/README.md index 319ffa0..99433c0 100644 --- a/examples/esp32/README.md +++ b/examples/esp32/README.md @@ -59,7 +59,7 @@ ```bash export AMPY_PORT=/dev/ttyUSB0 ampy mkdir /lib - ampy put blynklib.py /lib/blynklib.py + ampy put blynklib_mp.py /lib/blynklib_mp.py ampy put test.py test.py ampy run test.py ``` @@ -87,17 +87,17 @@ For **.mpy** file compilation you need: ``` - compile source code and get .mpy file ```bash - ./mpy-cross -X heapsize=2812256 blynklib.py + ./mpy-cross -X heapsize=2812256 blynklib_mp.py ``` - .mpy files in the same manner can be placed to board libs with **[ampy][micropython-ampy]** as usual .py file ```bash - ampy put blynklib.mpy /lib/blynklib.mpy + ampy put blynklib_mp.mpy /lib/blynklib_mp.mpy ``` and then imported within user scripts as usual .py lib ```python # start of user script - import blynklib + import blynklib_mp ``` ## Wifi Connection diff --git a/examples/esp8266/01_potentiometer.py b/examples/esp8266/01_potentiometer.py index 612bbbb..48339f0 100644 --- a/examples/esp8266/01_potentiometer.py +++ b/examples/esp8266/01_potentiometer.py @@ -36,7 +36,7 @@ http://twitter.com/blynk_app ===================================================================================================================== """ -import blynklib +import blynklib_mp as blynklib import network import utime as time import machine diff --git a/examples/esp8266/README.md b/examples/esp8266/README.md index 97c3356..d462356 100644 --- a/examples/esp8266/README.md +++ b/examples/esp8266/README.md @@ -59,7 +59,7 @@ ```bash export AMPY_PORT=/dev/ttyUSB0 ampy mkdir /lib - ampy put blynklib.py /lib/blynklib.py + ampy put blynklib_mp.py /lib/blynklib_mp.py ampy put test.py test.py ampy run test.py ``` @@ -94,7 +94,7 @@ For custom esp8266 firmware build creation: ```text RUN apt-get update ... ... - COPY blynklib.py /micropython/ports/esp8266/modules/blynklib.py + COPY blynklib_mp.py /micropython/ports/esp8266/modules/blynklib_mp.py USER micropython ... ``` @@ -103,10 +103,10 @@ For custom esp8266 firmware build creation: Build process can take some time ~ 15-40 minutes. - after firmware created and copied locally - you can try to burn it with **esptool** to your ESP8266 board. - - connect to board CLI with **rshell** and test **blynklib** availability within **repl** + - connect to board CLI with **rshell** and test **blynklib_mp** availability within **repl** ```python - import blynklib - print(blynklib.LOGO) + import blynklib_mp + print(blynklib_mp.LOGO) ``` @@ -118,13 +118,13 @@ Examine [this document][blynk-esp32-readme] to get more details how to compile * After *.mpy files can be placed to **/lib** directory of esp8266 board with **ampy** tool. Libraries *.mpy can be simply imported in the same manner as standard *.py library ```python -import blynklib +import blynklib_mp ``` ***Note!!*** During custom firmware creation your libraries will be converted and adopted to esp8266 environment automatically. So you can create custom build and then just copy *.mpy files from docker system to local ```bash -docker cp micropython:/micropython/ports/esp8266/build/frozen_mpy/blynklib.mpy blynklib.mpy +docker cp micropython:/micropython/ports/esp8266/build/frozen_mpy/blynklib_mp.mpy blynklib_mp.mpy ``` diff --git a/setup.py b/setup.py index 0421be7..144879d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='blynklib', - version='0.2.5', + version='0.2.6', description='Blynk Python/Micropython library', long_description=long_description, long_description_content_type="text/markdown", @@ -14,8 +14,8 @@ author='Anton Morozenko', author_email='antoha.ua@gmail.com', setup_requires=['pytest-runner', ], - tests_require=['pytest', 'pytest-mock', ], - py_modules=['blynklib', 'blynktimer'], + tests_require=['pytest', 'pytest-mock>=1.11.2', ], + py_modules=['blynklib', 'blynktimer', 'blynklib_mp'], classifiers=[ "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", diff --git a/test/test_blynk_connection.py b/test/test_blynk_connection.py index 74e4325..7c74430 100644 --- a/test/test_blynk_connection.py +++ b/test/test_blynk_connection.py @@ -12,91 +12,73 @@ def cb(self): connection = Connection('1234', log=print) yield connection - def test_set_socket_timeout_positive(self, cb): - in_timeout = 10 - cb._socket = socket.socket() - cb._set_socket_timeout(in_timeout) - timeout = cb._socket.gettimeout() - assert timeout == in_timeout - - def test_set_socket_timeout_via_poll(self, cb): - in_timeout = 10 - cb._socket = 2222 - cb._set_socket_timeout(in_timeout) - def test_send(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch('socket.socket.send', return_value=5): - result = cb.send('1234') - assert result == 5 + mocker.patch('socket.socket.send', return_value=5) + result = cb.send('1234') + assert result == 5 def test_send_ioerror(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch('socket.socket.send', side_effect=IOError('IO')): - result = cb.send('1234') - assert result is None + mocker.patch('socket.socket.send', side_effect=IOError('IO')) + result = cb.send('1234') + assert result is None def test_send_oserror(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch('socket.socket.send', side_effect=OSError('OS')): - result = cb.send('1234') - assert result is None + mocker.patch('socket.socket.send', side_effect=OSError('OS')) + result = cb.send('1234') + assert result is None def test_send_socket_timeout(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch('socket.socket.send', side_effect=socket.timeout()): - result = cb.send('1234') - assert result is None + mocker.patch('socket.socket.send', side_effect=socket.timeout()) + result = cb.send('1234') + assert result is None def test_send_error_retry_count(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch('socket.socket.send', side_effect=OSError('OS')): - mocker.spy(time, 'sleep') - cb.send('1234') - assert cb._socket.send.call_count == 3 + mocker.patch('socket.socket.send', side_effect=OSError('OS')) + mocker.spy(time, 'sleep') + cb.send('1234') + assert cb._socket.send.call_count == 3 def test_receive(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch.object(cb, '_set_socket_timeout', return_value=None): - with mocker.patch('socket.socket.recv', return_value=b'12345'): - result = cb.receive(10, 1) - assert result == b'12345' + mocker.patch('socket.socket.recv', return_value=b'12345') + result = cb.receive(10, 1) + assert result == b'12345' def test_receive_timeout(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch.object(cb, '_set_socket_timeout', return_value=None): - with mocker.patch('socket.socket.recv', side_effect=OSError('timed out')): - result = cb.receive(10, 1) - assert result == b'' + mocker.patch('socket.socket.recv', side_effect=OSError('timed out')) + result = cb.receive(10, 1) + assert result == b'' def test_receive_timeout_2(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch.object(cb, '_set_socket_timeout', return_value=None): - with mocker.patch('socket.socket.recv', side_effect=socket.timeout('timed out')): - result = cb.receive(10, 1) - assert result == b'' + mocker.patch('socket.socket.recv', side_effect=socket.timeout('timed out')) + result = cb.receive(10, 1) + assert result == b'' def test_receive_eagain(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch.object(cb, '_set_socket_timeout', return_value=None): - with mocker.patch('socket.socket.recv', side_effect=IOError('[Errno 11]')): - result = cb.receive(10, 1) - assert result == b'' + mocker.patch('socket.socket.recv', side_effect=IOError('[Errno 11]')) + result = cb.receive(10, 1) + assert result == b'' def test_receive_etimeout(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch.object(cb, '_set_socket_timeout', return_value=None): - with mocker.patch('socket.socket.recv', side_effect=OSError('[Errno 60]')): - result = cb.receive(10, 1) - assert result == b'' + mocker.patch('socket.socket.recv', side_effect=OSError('[Errno 60]')) + result = cb.receive(10, 1) + assert result == b'' def test_receive_raise_other_oserror(self, cb, mocker): cb._socket = socket.socket() - with mocker.patch.object(cb, '_set_socket_timeout', return_value=None): - with mocker.patch('socket.socket.recv', side_effect=OSError('[Errno 13]')): - with pytest.raises(OSError) as os_err: - cb.receive(10, 1) - assert '[Errno 13]' in str(os_err.value) + mocker.patch('socket.socket.recv', side_effect=OSError('[Errno 13]')) + with pytest.raises(OSError) as os_err: + cb.receive(10, 1) + assert '[Errno 13]' in str(os_err.value) def test_is_server_alive_negative(self, cb): result = cb.is_server_alive() @@ -104,9 +86,9 @@ def test_is_server_alive_negative(self, cb): def test_is_server_alive_positive_ping(self, cb, mocker): cb._last_rcv_time = int(time.time() * 1000) - with mocker.patch.object(cb, 'send', return_value=None): - result = cb.is_server_alive() - assert result is True + mocker.patch.object(cb, 'send', return_value=None) + result = cb.is_server_alive() + assert result is True def test_is_server_alive_positive_no_ping_1(self, cb): cb._last_rcv_time = int(time.time() * 1000) @@ -121,72 +103,72 @@ def test_is_server_alive_positive_no_ping_2(self, cb): assert result is True def test_get_socket(self, cb, mocker): - with mocker.patch('socket.socket'): - with mocker.patch('socket.getaddrinfo'): - cb._get_socket() - assert cb._state == cb.CONNECTING + mocker.patch('socket.socket') + mocker.patch('socket.getaddrinfo') + cb._get_socket() + assert cb._state == cb.CONNECTING def test_get_socket_exception(self, cb, mocker): - with mocker.patch('socket.socket'): - with mocker.patch('socket.getaddrinfo', side_effect=BlynkError('BE')): - with pytest.raises(BlynkError) as b_err: - cb._get_socket() - assert 'Connection with the Blynk server failed: BE' in str(b_err.value) + mocker.patch('socket.socket') + mocker.patch('socket.getaddrinfo', side_effect=BlynkError('BE')) + with pytest.raises(BlynkError) as b_err: + cb._get_socket() + assert 'Connection with the Blynk server failed: BE' in str(b_err.value) def test_authenticate(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\xc8'): - cb._authenticate() - assert cb._state == cb.AUTHENTICATED + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\xc8') + cb._authenticate() + assert cb._state == cb.AUTHENTICATED def test_authenticate_invalid_auth_token(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\x09'): - with pytest.raises(BlynkError) as b_err: - cb._authenticate() - assert 'Invalid Auth Token' in str(b_err.value) + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\x09') + with pytest.raises(BlynkError) as b_err: + cb._authenticate() + assert 'Invalid Auth Token' in str(b_err.value) def test_authenticate_redirect_message(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=b'\x29\x00\x02\x00\x11127.0.0.1\x004444'): - with pytest.raises(RedirectError) as r_err: - cb._authenticate() - # pytest exception wraps real exception - so we need access value field first - assert '127.0.0.1' in r_err.value.server - assert '4444' in r_err.value.port + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=b'\x29\x00\x02\x00\x11127.0.0.1\x004444') + with pytest.raises(RedirectError) as r_err: + cb._authenticate() + # pytest exception wraps real exception - so we need access value field first + assert '127.0.0.1' in r_err.value.server + assert '4444' in r_err.value.port def test_authenticate_not_ok_status(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\x19'): - with pytest.raises(BlynkError) as b_err: - cb._authenticate() - assert 'Auth stage failed. Status=25' in str(b_err.value) + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\x19') + with pytest.raises(BlynkError) as b_err: + cb._authenticate() + assert 'Auth stage failed. Status=25' in str(b_err.value) def test_authenticate_timeout(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=None): - with pytest.raises(BlynkError) as b_err: - cb._authenticate() - assert 'Auth stage timeout' in str(b_err.value) + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=None) + with pytest.raises(BlynkError) as b_err: + cb._authenticate() + assert 'Auth stage timeout' in str(b_err.value) def test_set_heartbeat_timeout(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=None): - with pytest.raises(BlynkError) as b_err: - cb._set_heartbeat() - assert 'Heartbeat stage timeout' in str(b_err.value) + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=None) + with pytest.raises(BlynkError) as b_err: + cb._set_heartbeat() + assert 'Heartbeat stage timeout' in str(b_err.value) def test_set_heartbeat_error_status(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\x0e'): - with pytest.raises(BlynkError) as b_err: - cb._set_heartbeat() - assert 'Set heartbeat returned code=14' in str(b_err.value) + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\x0e') + with pytest.raises(BlynkError) as b_err: + cb._set_heartbeat() + assert 'Set heartbeat returned code=14' in str(b_err.value) def test_set_heartbeat_positive(self, cb, mocker): - with mocker.patch.object(cb, 'send', return_value=None): - with mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\xc8'): - cb._set_heartbeat() + mocker.patch.object(cb, 'send', return_value=None) + mocker.patch.object(cb, 'receive', return_value=b'\x00\x00\x02\x00\xc8') + cb._set_heartbeat() def test_connected_false(self, cb): result = cb.connected() diff --git a/test/test_blynk_main.py b/test/test_blynk_main.py index 7913105..bff43bf 100644 --- a/test/test_blynk_main.py +++ b/test/test_blynk_main.py @@ -2,98 +2,105 @@ from __future__ import print_function import socket import pytest -from blynklib import Blynk, BlynkError, RedirectError +import blynklib class TestBlynk: @pytest.fixture def bl(self): - blynk = Blynk('1234', log=print) + blynk = blynklib.Blynk('1234', log=print) yield blynk def test_connect(self, bl, mocker): - with mocker.patch.object(bl, 'connected', return_value=False): - with mocker.patch.object(bl, '_get_socket', return_value=None): - with mocker.patch.object(bl, '_authenticate', return_value=None): - with mocker.patch.object(bl, '_set_heartbeat', return_value=None): - with mocker.patch.object(bl, 'call_handler', return_value=None): - result = bl.connect() - assert result is True + mocker.patch.object(bl, 'connected', return_value=False) + mocker.patch.object(bl, '_get_socket', return_value=None) + mocker.patch.object(bl, '_authenticate', return_value=None) + mocker.patch.object(bl, '_set_heartbeat', return_value=None) + mocker.patch.object(bl, 'call_handler', return_value=None) + mocker.patch.object(blynklib, 'ticks_ms', return_value=42) + result = bl.connect() + assert result is True + assert bl._last_rcv_time == 42 def test_connect_exception(self, bl, mocker): - with mocker.patch.object(bl, 'connected', return_value=False): - with mocker.patch.object(bl, '_get_socket', return_value=None): - with mocker.patch.object(bl, '_authenticate', side_effect=BlynkError()): - with mocker.patch.object(bl, 'disconnect', return_value=None): - with mocker.patch('time.sleep', return_value=None): - mocker.spy(bl, 'disconnect') - result = bl.connect(0.001) - assert result is False - assert bl.disconnect.call_count > 1 + mocker.patch.object(bl, 'connected', return_value=False) + mocker.patch.object(bl, '_get_socket', return_value=None) + mocker.patch.object(bl, '_authenticate', side_effect=blynklib.BlynkError()) + mocker.patch.object(bl, 'disconnect', return_value=None) + mocker.patch('time.sleep', return_value=None) + mocker.spy(bl, 'disconnect') + result = bl.connect(0.001) + assert result is False + assert bl.disconnect.call_count > 1 def test_connect_redirect_exception(self, bl, mocker): - with mocker.patch.object(bl, 'connected', return_value=False): - with mocker.patch.object(bl, '_get_socket', return_value=None): - with mocker.patch.object(bl, '_authenticate', side_effect=RedirectError('127.0.0.1', 4444)): - with mocker.patch.object(bl, 'disconnect', return_value=None): - with mocker.patch('time.sleep', return_value=None): - mocker.spy(bl, 'disconnect') - result = bl.connect(0.001) - assert result is False - assert bl.disconnect.call_count > 1 - assert bl.server == '127.0.0.1' - assert bl.port == 4444 + mocker.patch.object(bl, 'connected', return_value=False) + mocker.patch.object(bl, '_get_socket', return_value=None) + mocker.patch.object(bl, '_authenticate', side_effect=blynklib.RedirectError('127.0.0.1', 4444)) + mocker.patch.object(bl, 'disconnect', return_value=None) + mocker.patch('time.sleep', return_value=None) + mocker.spy(bl, 'disconnect') + result = bl.connect(0.001) + assert result is False + assert bl.disconnect.call_count > 1 + assert bl.server == '127.0.0.1' + assert bl.port == 4444 def test_connect_timeout(self, bl, mocker): bl._state = bl.CONNECTING - with mocker.patch.object(bl, 'connected', return_value=False): - result = bl.connect(0.001) - assert result is False + mocker.patch.object(bl, 'connected', return_value=False) + result = bl.connect(0.001) + assert result is False def test_disconnect(self, bl, mocker): bl._socket = socket.socket() - with mocker.patch('time.sleep', return_value=None): - bl.disconnect('123') + mocker.patch('time.sleep', return_value=None) + bl.disconnect('123') def test_virtual_write(self, bl, mocker): - with mocker.patch.object(bl, 'send', return_value=10): - result = bl.virtual_write(20, 'va1', 'val2') - assert result == 10 + mocker.patch.object(bl, 'send', return_value=10) + result = bl.virtual_write(20, 'va1', 'val2') + assert result == 10 def test_virtual_sync(self, bl, mocker): - with mocker.patch.object(bl, 'send', return_value=20): - result = bl.virtual_sync(20, 22) - assert result == 20 + mocker.patch.object(bl, 'send', return_value=20) + result = bl.virtual_sync(20, 22) + assert result == 20 def test_email(self, bl, mocker): - with mocker.patch.object(bl, 'send', return_value=30): - result = bl.email('1', '2', '3') - assert result == 30 + mocker.patch.object(bl, 'send', return_value=30) + result = bl.email('1', '2', '3') + assert result == 30 def test_tweet(self, bl, mocker): - with mocker.patch.object(bl, 'send', return_value=40): - result = bl.tweet('123') - assert result == 40 + mocker.patch.object(bl, 'send', return_value=40) + result = bl.tweet('123') + assert result == 40 def test_notify(self, bl, mocker): - with mocker.patch.object(bl, 'send', return_value=50): - result = bl.notify('123') - assert result == 50 + mocker.patch.object(bl, 'send', return_value=50) + result = bl.notify('123') + assert result == 50 def test_set_property(self, bl, mocker): - with mocker.patch.object(bl, 'send', return_value=60): - result = bl.set_property(1, '2', '3') - assert result == 60 + mocker.patch.object(bl, 'send', return_value=60) + result = bl.set_property(1, '2', '3') + assert result == 60 + + def test_internal(self, bl, mocker): + mocker.patch.object(bl, 'send', return_value=70) + result = bl.internal('rtc', 'sync') + assert result == 70 def test_hadle_event(self, bl): bl._events = {} - @bl.handle_event("connect") + @bl.handle_event('connect') def connect_handler(): pass - @bl.handle_event("disconnect") - def connect_handler(): + @bl.handle_event('disconnect') + def disconnect_handler(): pass assert 'connect' in bl._events.keys() @@ -135,21 +142,21 @@ def test_process_rsp(self, bl, mocker): assert bl.log.call_count == 1 def test_process_ping(self, bl, mocker): - with mocker.patch.object(bl, 'send', return_value=None): - mocker.spy(bl, 'send') - bl.process(bl.MSG_PING, 100, 200, []) - assert bl.send.call_count == 1 + mocker.patch.object(bl, 'send', return_value=None) + mocker.spy(bl, 'send') + bl.process(bl.MSG_PING, 100, 200, []) + assert bl.send.call_count == 1 def test_process_internal(self, bl, mocker): bl._events = {} - @bl.handle_event('internal_5') + @bl.handle_event('internal_xyz') def internal_handler(*args): - bl._status = 'INTERNAL TEST{}'.format(*args) + bl._status = 'INTERNAL TEST {}'.format(*args) - with mocker.patch.object(bl, 'send', return_value=None): - bl.process(bl.MSG_INTERNAL, 100, 200, ['int', 5, 1, 2]) - assert bl._status == 'INTERNAL TEST[1, 2]' + mocker.patch.object(bl, 'send', return_value=None) + bl.process(bl.MSG_INTERNAL, 100, 20, ['xyz', 'add', 2]) + assert bl._status == "INTERNAL TEST ['add', 2]" def test_process_write(self, bl, mocker): bl._events = {} @@ -158,9 +165,9 @@ def test_process_write(self, bl, mocker): def write_handler(pin, *values): bl._status = 'WRITE TEST{}'.format(*values) - with mocker.patch.object(bl, 'send', return_value=None): - bl.process(bl.MSG_HW, 100, 200, ['vw', 4, 1, 2]) - assert bl._status == 'WRITE TEST[1, 2]' + mocker.patch.object(bl, 'send', return_value=None) + bl.process(bl.MSG_HW, 100, 200, ['vw', 4, 1, 2]) + assert bl._status == 'WRITE TEST[1, 2]' def test_process_read(self, bl, mocker): bl._events = {} @@ -169,6 +176,6 @@ def test_process_read(self, bl, mocker): def read_handler(pin): bl._status = 'READ TEST{}'.format(pin) - with mocker.patch.object(bl, 'send', return_value=None): - bl.process(bl.MSG_HW, 100, 200, ['vr', 7]) - assert bl._status == 'READ TEST7' + mocker.patch.object(bl, 'send', return_value=None) + bl.process(bl.MSG_HW, 100, 200, ['vr', 7]) + assert bl._status == 'READ TEST7' diff --git a/test/test_blynk_protocol.py b/test/test_blynk_protocol.py index ceee9ae..a838e33 100644 --- a/test/test_blynk_protocol.py +++ b/test/test_blynk_protocol.py @@ -32,24 +32,24 @@ def test_get_msg_id_defined(self, pb): def test_pack_msg(self, pb): msg_type = 20 - args = ["test", 1234, 745, 'abcde'] + args = ['test', 1234, 745, 'abcde'] result = pb._pack_msg(msg_type, *args) - assert result == b'\x14\x00\x02\x00\x13test\x001234\x00745\x00abcde' + assert result == b'\x14\x00\x01\x00\x13test\x001234\x00745\x00abcde' def test_pack_msg_no_args(self, pb): msg_type = 15 args = [] result = pb._pack_msg(msg_type, *args) - assert result == b'\x0f\x00\x02\x00\x00' + assert result == b'\x0f\x00\x01\x00\x00' def test_pack_msg_unicode(self, pb): if sys.version_info[0] == 2: pytest.skip('Python2 unicode compatibility issue') msg_type = 20 - args = ["ёж"] + args = ['ёж'] result = pb._pack_msg(msg_type, *args) - assert result == b'\x14\x00\x02\x00\x04\xd1\x91\xd0\xb6' + assert result == b'\x14\x00\x01\x00\x04\xd1\x91\xd0\xb6' def test_parse_response_msg_hw(self, pb): data = b'\x14\x00\x02\x00\x13test\x001234\x00745\x00abcde' @@ -99,40 +99,44 @@ def test_parse_response_msg_hw_unicode(self, pb): def test_heartbeat_msg(self, pb): result = pb.heartbeat_msg(20, 2048) - assert result == b'\x11\x00\x02\x00+ver\x000.2.5\x00buff-in\x002048\x00h-beat\x0020\x00dev\x00python' + assert result == b'\x11\x00\x01\x00+ver\x000.2.6\x00buff-in\x002048\x00h-beat\x0020\x00dev\x00python' def test_login_msg(self, pb): result = pb.login_msg('1234') - assert result == b'\x02\x00\x02\x00\x041234' + assert result == b'\x02\x00\x01\x00\x041234' def test_ping_msg(self, pb): result = pb.ping_msg() - assert result == b'\x06\x00\x02\x00\x00' + assert result == b'\x06\x00\x01\x00\x00' def test_response_msg(self, pb): result = pb.response_msg(202) - assert result == b'\x00\x00\x02\x00\x03202' + assert result == b'\x00\x00\x01\x00\x03202' def test_virtual_write_msg(self, pb): result = pb.virtual_write_msg(127, 'abc', 123) - assert result == b'\x14\x00\x02\x00\x0evw\x00127\x00abc\x00123' + assert result == b'\x14\x00\x01\x00\x0evw\x00127\x00abc\x00123' def test_virtual_sync_msg(self, pb): result = pb.virtual_sync_msg(1, 24) - assert result == b'\x10\x00\x02\x00\x07vr\x001\x0024' + assert result == b'\x10\x00\x01\x00\x07vr\x001\x0024' def test_email_msg(self, pb): result = pb.email_msg('a@b.com', 'Test', 'MSG') - assert result == b'\r\x00\x02\x00\x10a@b.com\x00Test\x00MSG' + assert result == b'\r\x00\x01\x00\x10a@b.com\x00Test\x00MSG' def test_tweet_msg(self, pb): result = pb.tweet_msg('tweet_msg_test') - assert result == b'\x0c\x00\x02\x00\x0etweet_msg_test' + assert result == b'\x0c\x00\x01\x00\x0etweet_msg_test' def test_notify_msg(self, pb): result = pb.notify_msg('app_msg_test') - assert result == b'\x0e\x00\x02\x00\x0capp_msg_test' + assert result == b'\x0e\x00\x01\x00\x0capp_msg_test' def test_set_property_msg(self, pb): result = pb.set_property_msg(10, 'color', '#FF00EE') - assert result == b'\x13\x00\x02\x00\x1010\x00color\x00#FF00EE' + assert result == b'\x13\x00\x01\x00\x1010\x00color\x00#FF00EE' + + def test_internal_msg(self, pb): + result = pb.internal_msg('rtc', 'sync') + assert result == b'\x11\x00\x01\x00\x08rtc\x00sync'