diff --git a/LICENSE b/LICENSE.txt similarity index 96% rename from LICENSE rename to LICENSE.txt index c1c093a..812fb5e 100644 --- a/LICENSE +++ 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 3596826..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. @@ -36,7 +42,7 @@ Full list of supported hardware can be found [here][blynk-hw]. To exclude compatibility issue preferable versions are Python 2.7.9 (or greater) or Python 3.4 (or greater) If python not present you can download and install it from [here][python-org]. - **NOTE:** To run python in "sandbox" you can try **virtualenv** module. Check [here][virtual-env] on how to do it. + **NOTE:** To run python in "sandbox" you can try **virtualenv** module. Check [this document][virtual-env] how to do it. - If you’re using preferable versions of python mentioned above, then **pip** comes installed with Python by default. Check pip availability: @@ -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 @@ -101,13 +109,14 @@ This library supports Python2, Python3, and Micropython. - You will get Auth Token delivered to your email account. - Put this Auth Token within your python script to authenticate your device on [public][blynk-server-public] or [local][blynk-server] -```py +```python BLYNK_AUTH = '' #insert your Auth Token here ``` #### Usage example -```py +```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) @@ -149,29 +165,37 @@ while True: ``` ## Other Examples -Check **[more_examples][blynk-py-examples]** to get familiar with some of Blynk features. - -#### Examples List: +Examples can be found **[here][blynk-py-examples]** Check them all to get familiar with main Blynk API features. ##### Core operations: -- 01_write_virtual_pin.py (how to write to Virtual Pin ) -- 02_read_virtual_pin.py (how to read Virtual Pin ) -- 03_connect_disconnect.py (connection management) -- 04_email.py(how to send send email and push notifications) -- 05_set_property_notify.py (how to change some of widget UI properties) -- 06_terminal_widget.py (communication between hardware and app through Terminal widget) -- 07_tweet_and_logging.py (how to post to Twitter and log events from your hardware) +- [01_write_virtual_pin.py](https://github.com/blynkkk/lib-python/blob/master/examples/01_write_virtual_pin.py): How to read incoming data from Blynk app to Virtual Pin and use it in your code +- [02_read_virtual_pin.py](https://github.com/blynkkk/lib-python/blob/master/examples/02_read_virtual_pin.py): How to update value on Virtual Pin +- [03_connect_disconnect.py](https://github.com/blynkkk/lib-python/blob/master/examples/03_connect_disconnect.py): Managing connection with Blynk Cloud +- [04_email.py](https://github.com/blynkkk/lib-python/blob/master/examples/04_email.py): How to send send email and push notifications from your hardware +- [05_set_property_notify.py](https://github.com/blynkkk/lib-python/blob/master/examples/05_set_property_notify.py): How to change some of widget UI properties like colors, labels, etc +- [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 +- [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): -- 01_weather_station_pi3b.py (connect DHT22; BMP180 sensors and send data to Blynk app) +Read [Raspberry Pi guide](https://github.com/blynkkk/lib-python/tree/master/examples/raspberry) first. + +- [01_weather_station_pi3b.py](https://github.com/blynkkk/lib-python/blob/master/examples/raspberry/01_weather_station_pi3b.py) Connect DHT22; BMP180 sensors and send data to Blynk app ##### ESP32 -- 01_touch_button.py (connect TTP223B touch sensor to ESP32 and react to touch) -- 02_terminal_cli.py (communication between ESP32 hardware and app through Terminal widget) -- 03_temperature_humidity_dht22.py (connect DHT22 sensor and send data to Blynk app) +Read [ESP32 guide](https://github.com/blynkkk/lib-python/tree/master/examples/esp32) first. +- [01_touch_button.py](https://github.com/blynkkk/lib-python/blob/master/examples/esp32/01_touch_button.py) Connect TTP223B touch sensor to ESP32 and react to touch +- [02_terminal_cli.py](https://github.com/blynkkk/lib-python/blob/master/examples/esp32/02_terminal_cli.py) Communication between ESP32 hardware and app through Terminal widget +- [03_temperature_humidity_dht22.py](https://github.com/blynkkk/lib-python/blob/master/examples/esp32/03_temperature_humidity_dht22.py) Connect DHT22 sensor to ESP32 and send data to Blynk app ##### ESP8266 -- 01_potentiometer.py (connect slide potentiometer to ESP8266 and get resistance values) +Read [ESP8266 guide](https://github.com/blynkkk/lib-python/tree/master/examples/esp8266) first. +- [01_potentiometer.py](https://github.com/blynkkk/lib-python/blob/master/examples/esp8266/01_potentiometer.py) Cconnect potentiometer to ESP8266 and send resistance value to the app @@ -181,6 +205,36 @@ to load **blynklib** or any other library to hardware. Read [this document][esp8266-readme] to get more information. +## Documentation and other helpful links + +[Full Blynk Documentation](https://docs.blynk.io) - a complete guide on Blynk features + +[Community (Forum)](https://community.blynk.cc) - join a 1'000'000 Blynk community to ask questions and share ideas + +[Official Website](https://blynk.io) + +**Social Media:** + +[Facebook](https://www.fb.com/blynkapp) [Twitter](https://twitter.com/blynk_app) [Youtube](https://www.youtube.com/blynk) + +[Instagram](https://www.instagram.com/blynk.iot/) [LinkedIn](https://www.linkedin.com/company/b-l-y-n-k/) + + +## Blynk libraries for other platforms +* [C++](https://github.com/blynkkk/blynk-library) +* [Node.js, Espruino, Browsers](https://github.com/vshymanskyy/blynk-library-js) +* [Python](https://github.com/vshymanskyy/blynk-library-python) (by Volodymyr Shymanskyy) +* [Particle](https://github.com/vshymanskyy/blynk-library-spark) +* [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 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) + +## Contributing +You are very welcome to contribute: stability bugfixes, new hardware support, or any other improvements. Please. + ### License This project is released under The MIT License (MIT) @@ -200,11 +254,12 @@ 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/ [micropython-pkg]: https://github.com/micropython/micropython/wiki/Getting-Started [virtual-env]: https://virtualenv.pypa.io/en/latest/installation/ [esp8266-readme]: https://github.com/blynkkk/lib-python/blob/master/examples/esp8266/README.md + [blynktimer-doc]: https://github.com/blynkkk/lib-python/blob/master/TIMERS.md diff --git a/TIMERS.md b/TIMERS.md new file mode 100644 index 0000000..371f966 --- /dev/null +++ b/TIMERS.md @@ -0,0 +1,96 @@ +# Blynk Timers +There are two options of setting polling timers in **blynk** + + - create timers for your functions on hardware side + - create timers on Blynk App side. + +### Hardware timers +Existing core library solutions may be helpful for hardware timers creation. + + For example: + - micropython provides [machine.Timer][micropython-timer] + - for cPython [threading.Timer][threading-timer] can be used + - etc + + Unfortunately mentioned above solutions may be not so lightweight and clear as expected. + For Quickstart we provide separate [timer module][blynktimer] that allows execute functions periodically or run them once. + +##### Basic usage examples +```python +from blynktimer import Timer +blynk_timer = Timer() + +# run once timer that will fire after 1 sec +@blynk_timer.register(interval=1, run_once=True) +def your_run_once_function(): + print('Hello, World!') + +# periodical timer that will fire each 5 sec +# run_once flag by default is False +@blynk_timer.register(interval=5) +def your_periodical_function(): + print('Hello, Blynkers!') + +while True: + blynk_timer.run() + +``` + +##### Advanced usage examples +```python +import time +from blynktimer import Timer + +# disable exception raise if all all timers were stopped +blynk_timer = Timer(no_timers_err=False) + + +# register two timers for single function with different function parameters +@blynk_timer.register('p1', 'p2', c=1, interval=2, run_once=True) +@blynk_timer.register('fp1', 'fp2', interval=3, run_once=False) +def function1(a, b, c=2): + time.sleep(c) + print('Function params: {} {} {}'.format(a, b, c)) + + +# simple function registration for further stop +# interval default = 10 sec +# run_once default is False +@blynk_timer.register() +def function2(): + print('Function2') + + +# list available timers +print(blynk_timer.get_timers()) + +# switch timer state to stopped by timer id +# id = order_num + '_' + function_name +# OR: on ports with low memory (such as the esp8266) +# id = order_num + '_' + 'timer' +blynk_timer.stop('2_function2') + + +while True: + intervals = blynk_timer.run() + # print real passed time for timer fired events + # maybe needed for debug + if any(intervals): + print(intervals) +``` + +To get more accuracy for timers intervals it is possible to decrease library WAIT_SEC parameter. Default value = 0.05 sec + +### Blynk App timers +Some Blynk app widgets have timer setting where yoy can define (1,2,5,10 etc) seconds intervals for reading +virtual pin values. + +Flow: + - each N seconds Blynk app widget will do Virtual Pin reading operation. + - Blynk Server for App read request will return current pin value + - Additionally Blynk server will fire read virtual pin event and send it to hardware + - If read pin event was registered on hardware certain handler will be executed + + [micropython-timer]: https://docs.micropython.org/en/latest/library/machine.Timer.html + [threading-timer]:https://docs.python.org/3/library/threading.html#threading.Timer + [blynktimer]: https://github.com/blynkkk/lib-python/blob/master/blynktimer.py \ No newline at end of file diff --git a/blynklib.py b/blynklib.py index af20e1a..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.4' +__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,29 +21,45 @@ 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 +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_HEAD_LEN = const(5) - - STATUS_INVALID_TOKEN = const(9) - 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: @@ -81,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): + 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: @@ -121,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 @@ -142,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 @@ -171,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'' @@ -202,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)) @@ -214,10 +221,12 @@ def _authenticate(self): rsp_data = self.receive(self.rcv_buffer, self.SOCK_MAX_TIMEOUT) if not rsp_data: raise BlynkError('Auth stage timeout') - _, _, status, _ = self.parse_response(rsp_data, self.rcv_buffer) + 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') @@ -237,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' @@ -265,23 +274,30 @@ 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 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)) + self._msg_id = 0 time.sleep(self.RECONNECT_SLEEP) - self.call_handler(self._DISCONNECT) def virtual_write(self, v_pin, *val): return self.send(self.virtual_write_msg(v_pin, *val)) @@ -301,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): @@ -308,7 +327,7 @@ def __init__(self, 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(1, blynk.VPIN_MAX_NUM + 1): + 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 @@ -329,23 +348,28 @@ 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) + 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) + def run(self): if not self.connected(): self.connect() else: try: - rsp_data = self.receive(self.rcv_buffer, self.SOCK_TIMEOUT) - self._last_rcv_time = ticks_ms() - if rsp_data: - 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) + self.read_response(timeout=self.SOCK_TIMEOUT) if not self.is_server_alive(): self.disconnect('Blynk server is offline') except KeyboardInterrupt: 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 new file mode 100644 index 0000000..704a391 --- /dev/null +++ b/blynktimer.py @@ -0,0 +1,133 @@ +# Copyright (c) 2019-2020 Anton Morozenko +""" +Polling timers for functions. +Registers timers and performs run once or periodical function execution after defined time intervals. +""" +# select.select call used as polling waiter where it is possible +# cause time.sleep sometimes may load CPU up to 100% with small polling wait interval +try: + # cpython + import time + import select + + polling_wait = lambda x: select.select([], [], [], x) + polling_wait(0.01) +except OSError: + # windows case where select.select call fails + polling_wait = lambda x: time.sleep(x) + +except ImportError: + # micropython + import utime as time + + try: + from uselect import select as s_select + + polling_wait = lambda x: s_select([], [], [], x) + except ImportError: + # case when micropython port does not support select.select + polling_wait = lambda x: time.sleep(x) + +WAIT_SEC = 0.05 +MAX_TIMERS = 16 +DEFAULT_INTERVAL = 10 + + +class TimerError(Exception): + pass + + +class Timer(object): + timers = {} + + def __init__(self, no_timers_err=True): + self.no_timers_err = no_timers_err + + def _get_func_name(self, obj): + """retrieves a suitable name for a function""" + if hasattr(obj, 'func'): + # handles nested decorators + return self._get_func_name(obj.func) + # simply returns 'timer' if on port without function attrs + return getattr(obj, '__name__', 'timer') + + def register(blynk, *args, **kwargs): + # kwargs with defaults are used cause PEP 3102 no supported by Python2 + interval = kwargs.pop('interval', DEFAULT_INTERVAL) + run_once = kwargs.pop('run_once', False) + stopped = kwargs.pop('stopped', False) + + class Deco(object): + def __init__(self, func): + self.func = func + if len(list(Timer.timers.keys())) >= MAX_TIMERS: + raise TimerError('Max allowed timers num={}'.format(MAX_TIMERS)) + _timer = _Timer(interval, func, run_once, stopped, *args, **kwargs) + Timer.timers['{}_{}'.format(len(list(Timer.timers.keys())), blynk._get_func_name(func))] = _timer + + def __call__(self, *f_args, **f_kwargs): + return self.func(*f_args, **f_kwargs) + + return Deco + + @staticmethod + def stop(t_id): + timer = Timer.timers.get(t_id, None) + if timer is None: + raise TimerError('Timer id={} not found'.format(t_id)) + Timer.timers[t_id].stopped = True + + @staticmethod + def start(t_id): + timer = Timer.timers.get(t_id, None) + if timer is None: + raise TimerError('Timer id={} not found'.format(t_id)) + Timer.timers[t_id].stopped = False + Timer.timers[t_id].fire_time = None + Timer.timers[t_id].fire_time_prev = None + + @staticmethod + def is_stopped(t_id): + timer = Timer.timers.get(t_id, None) + if timer is None: + raise TimerError('Timer id={} not found'.format(t_id)) + return timer.stopped + + def get_timers(self): + states = {True: 'Stopped', False: 'Running'} + return {k: states[v.stopped] for k, v in self.timers.items()} + + def run(self): + polling_wait(WAIT_SEC) + timers_intervals = [curr_timer.run() for curr_timer in Timer.timers.values() if not curr_timer.stopped] + if not timers_intervals and self.no_timers_err: + raise TimerError('Running timers not found') + return timers_intervals + + +class _Timer(object): + def __init__(self, interval, deco, run_once, stopped, *args, **kwargs): + self.interval = interval + self.deco = deco + self.args = args + self.run_once = run_once + self.kwargs = kwargs + self.fire_time = None + self.fire_time_prev = None + self.stopped = stopped + + def run(self): + timer_real_interval = 0 + if self.fire_time is None: + self.fire_time = time.time() + self.interval + if self.fire_time_prev is None: + self.fire_time_prev = time.time() + curr_time = time.time() + if curr_time >= self.fire_time: + self.deco(*self.args, **self.kwargs) + if self.run_once: + self.stopped = True + timer_real_interval = curr_time - self.fire_time_prev + self.fire_time_prev = self.fire_time + self.fire_time = curr_time + self.interval + return timer_real_interval 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/01_write_virtual_pin.py b/examples/01_write_virtual_pin.py index c2cf513..04a688c 100644 --- a/examples/01_write_virtual_pin.py +++ b/examples/01_write_virtual_pin.py @@ -44,7 +44,7 @@ ==================================================================================================== Additional blynk info you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 diff --git a/examples/02_read_virtual_pin.py b/examples/02_read_virtual_pin.py index 5019cd8..f45850b 100644 --- a/examples/02_read_virtual_pin.py +++ b/examples/02_read_virtual_pin.py @@ -47,7 +47,7 @@ ==================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 diff --git a/examples/03_connect_disconnect.py b/examples/03_connect_disconnect.py index c4df093..da3154b 100644 --- a/examples/03_connect_disconnect.py +++ b/examples/03_connect_disconnect.py @@ -43,7 +43,7 @@ ==================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 @@ -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/04_email.py b/examples/04_email.py index 8be07d0..d3611d6 100644 --- a/examples/04_email.py +++ b/examples/04_email.py @@ -44,7 +44,7 @@ ===================================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 diff --git a/examples/05_set_property_notify.py b/examples/05_set_property_notify.py index 5c13a11..50f39d8 100644 --- a/examples/05_set_property_notify.py +++ b/examples/05_set_property_notify.py @@ -54,7 +54,7 @@ ===================================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 @@ -74,8 +74,8 @@ @blynk.handle_event('write V5') def write_handler(pin, value): - current_color = random.choice(colors.keys()) - blynk.set_property(pin, 'color', random.choice(colors.keys())) + current_color = random.choice(list(colors.keys())) + blynk.set_property(pin, 'color', current_color) blynk.notify(NOTIFY_MSG.format(colors[current_color])) print(NOTIFY_MSG.format(colors[current_color])) diff --git a/examples/06_terminal_widget.py b/examples/06_terminal_widget.py index 2dccac1..1b2d804 100644 --- a/examples/06_terminal_widget.py +++ b/examples/06_terminal_widget.py @@ -48,7 +48,7 @@ ===================================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 diff --git a/examples/07_tweet_and_logging.py b/examples/07_tweet_and_logging.py index 094266d..bf09e4b 100644 --- a/examples/07_tweet_and_logging.py +++ b/examples/07_tweet_and_logging.py @@ -46,7 +46,7 @@ ===================================================================================================================== Additional blynk info you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 diff --git a/examples/08_blynk_timer.py b/examples/08_blynk_timer.py new file mode 100644 index 0000000..c434588 --- /dev/null +++ b/examples/08_blynk_timer.py @@ -0,0 +1,79 @@ +""" +[BLYNK TIMER EXAMPLE] ============================================================================================ + +Environment prepare: +In your Blynk App project: + - add "SuperChart" widget + - add two data streams for it ( stream #1 - Virtual Pin 8, stream #2 - Virtual Pin 9) + - set different colors for data stream (ex. red and orange) + - Run the App (green triangle in the upper right corner). + - define your auth token for current example and run it + + +This started program will register two timers that will update Virtual Pins data after defined intervals. +In app you can see on graph both pins data change. Additionally timers will print new pin values info to stdout. + +Schema: +================================================================================================================= + +-----------+ +--------------+ +--------------+ + | | | | | | + | blynk lib | | blynk server | | blynk app | + | | | virtual pin | | | + | | | | | | + +-----+-----+ +------+-------+ +-------+------+ + | | | + | | | + | | | + | | | ++------------+ | | +| +---------+ | | +| | | update virtual pin 8 value | | +| | | | notify app about vpin 8 update | +| +-------->------------------------------------->+ | +| | +----------------------------------->+ ++----------->------------------------------------->+ | + | +----------------------------------->+ + | update virtual pin 9 value | | + | | notify app about vpin 9 update | + | | | + | | | + | | | + + + + + +================================================================================================================ +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 +import blynktimer +import random + +BLYNK_AUTH = 'YourAuthToken' # insert your Auth Token here +blynk = blynklib.Blynk(BLYNK_AUTH) + +# create timers dispatcher instance +timer = blynktimer.Timer() + +WRITE_EVENT_PRINT_MSG = "[WRITE_VIRTUAL_WRITE] Pin: V{} Value: '{}'" + + +# Code below: register two timers for different pins with different intervals +# run_once flag allows to run timers once or periodically +@timer.register(vpin_num=8, interval=4, run_once=False) +@timer.register(vpin_num=9, interval=7, run_once=False) +def write_to_virtual_pin(vpin_num=1): + value = random.randint(0, 20) + print(WRITE_EVENT_PRINT_MSG.format(vpin_num, value)) + blynk.virtual_write(vpin_num, value) + + +while True: + blynk.run() + timer.run() diff --git a/examples/09_sync_virtual_pin.py b/examples/09_sync_virtual_pin.py new file mode 100644 index 0000000..5e6769f --- /dev/null +++ b/examples/09_sync_virtual_pin.py @@ -0,0 +1,112 @@ +""" +[VIRTUAL PIN SYNC EXAMPLE] ========================================================================================== + +Environment prepare: +In your Blynk App project: + - add 3 "Button" widgets, + - bind then to Virtual Pins V0, V1, v2 + - set mode "SWITCH" for all of them + - Run the App (green triangle in the upper right corner). + - define your auth token for current example and run it + + +This started program will restore on connect write_virtual_pin states and colors. +Buttons states and colors can be modified during script run. +If script was interrupted (KeyboardInterrupt) buttons colors will be changed to red. +During next connect event ( script re-run) previous buttons states and colors will be restored. + +Schema: +===================================================================================================================== + +-----------+ +--------------+ +--------------+ + | | | | | | + | blynk lib | | blynk server | | blynk app | + | | | virtual pin | | | + | | | | | | + +-----+-----+ +------+-------+ +-------+------+ +connect handler | | | + +--------+ | | + | +------------------------------------>+ | + | | virtual pin sync | | + | +<------------------------------------+----------------------------------->+ + | | virtual pin write stored value | send pin value to app | + | +<------------------------------------+----------------------------------->+ + | | virtual pin apply stored properties | send pin properties to app | + +------->+ | | + write handler | | | + +--------+ +<-----------------------------------+ + | +<------------------------------------+ write event from button | + +>------>+ write event form server | | + | | | + | | | + disconnect | | | + handler | | | + +-------+ | | + | +------------------------------------>+ | + +------>+ set new virtual pin property | | + | | | + | | | + | | | + + + + + +===================================================================================================================== +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 logging +import blynklib + +# 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) + +colors = {'1': '#FFC300', '0': '#CCCCCC', 'OFFLINE': '#FF0000'} + +BLYNK_AUTH = 'YourAuthToken' +blynk = blynklib.Blynk(BLYNK_AUTH, log=_log.info) + + +@blynk.handle_event("connect") +def connect_handler(): + _log.info('SCRIPT_START') + for pin in range(3): + _log.info('Syncing virtual pin {}'.format(pin)) + blynk.virtual_sync(pin) + + # within connect handler after each server send operation forced socket reading is required cause: + # - we are not in script listening state yet + # - without forced reading some portion of blynk server messages can be not delivered to HW + blynk.read_response(timeout=0.5) + + +@blynk.handle_event('write V*') +def write_handler(pin, value): + button_state = value[0] + blynk.set_property(pin, 'color', colors[button_state]) + + +@blynk.handle_event("disconnect") +def connect_handler(): + for pin in range(3): + _log.info("Set 'OFFLINE' color for pin {}".format(pin)) + blynk.set_property(pin, 'color', colors['OFFLINE']) + + +########################################################### +# infinite loop that waits for event +########################################################### +try: + while True: + blynk.run() +except KeyboardInterrupt: + blynk.disconnect() + _log.info('SCRIPT WAS INTERRUPTED') 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 15267ed..c831a70 100644 --- a/examples/esp32/01_touch_button.py +++ b/examples/esp32/01_touch_button.py @@ -27,14 +27,14 @@ ===================================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 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 6a30668..9fa33a8 100644 --- a/examples/esp32/02_terminal_cli.py +++ b/examples/esp32/02_terminal_cli.py @@ -26,14 +26,14 @@ ===================================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 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 a4ea3c9..87c3c0d 100644 --- a/examples/esp32/03_temperature_humidity_dht22.py +++ b/examples/esp32/03_temperature_humidity_dht22.py @@ -41,14 +41,14 @@ ===================================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 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/examples/raspberry/01_weather_station_pi3b.py b/examples/raspberry/01_weather_station_pi3b.py index 8ceae01..d13d92d 100644 --- a/examples/raspberry/01_weather_station_pi3b.py +++ b/examples/raspberry/01_weather_station_pi3b.py @@ -106,7 +106,7 @@ ===================================================================================================================== Additional info about blynk you can find by examining such resources: - Downloads, docs, tutorials: http://www.blynk.cc + 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 diff --git a/setup.py b/setup.py index 7de2cac..144879d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='blynklib', - version='0.2.4', + 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'], + 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 02e2003..7c74430 100644 --- a/test/test_blynk_connection.py +++ b/test/test_blynk_connection.py @@ -3,7 +3,7 @@ import time import pytest import socket -from blynklib import Connection, BlynkError +from blynklib import Connection, BlynkError, RedirectError class TestBlynkConnection: @@ -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) + 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,63 +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) + 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) + 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): + 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) + 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) + 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) + 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) + 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 a092c0b..bff43bf 100644 --- a/test/test_blynk_main.py +++ b/test/test_blynk_main.py @@ -2,85 +2,105 @@ from __future__ import print_function import socket import pytest -from blynklib import Blynk, BlynkError +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): + 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() @@ -94,7 +114,7 @@ def read_pin_handler(): pass assert 'read v10' in bl._events.keys() - assert len(bl._events.keys()) == bl.VPIN_MAX_NUM + assert len(bl._events.keys()) == bl.VPIN_MAX_NUM + 1 def test_write_wildcard_event(self, bl): bl._events = {} @@ -104,7 +124,7 @@ def write_pin_handler(): pass assert 'write v5' in bl._events.keys() - assert len(bl._events.keys()) == bl.VPIN_MAX_NUM + assert len(bl._events.keys()) == bl.VPIN_MAX_NUM + 1 def test_call_handler(self, bl): bl._events = {} @@ -122,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 = {} @@ -145,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 = {} @@ -156,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 7853a8a..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.4\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'