├── tools ├── inc │ └── database │ │ ├── __init__.py │ │ └── db_sqlite.py ├── default.conf └── server.py ├── lib ├── __init__.py ├── common.py ├── server │ └── __init__.py ├── hw.py └── client.py ├── README.md ├── examples └── hw.py └── LICENSE /tools/inc/database/__init__.py: -------------------------------------------------------------------------------- 1 | def createFromConf(conf): 2 | if conf['type']=='sqlite': 3 | import db_sqlite 4 | return db_sqlite.Database(conf['conf']) 5 | -------------------------------------------------------------------------------- /tools/default.conf: -------------------------------------------------------------------------------- 1 | server: 2 | type: tcp 3 | conf: 4 | port: 8442 5 | net: 0.0.0.0 6 | max_user_connections: 10 7 | user_timeout: 300 8 | 9 | database: 10 | type: sqlite 11 | conf: 12 | file: /etc/blynk/server.db 13 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pyblynk package 4 | """ 5 | __author__ = """Alexander Krause """ 6 | __date__ = "2016-01-11" 7 | __version__ = "0.2.0" 8 | __credits__ = """Copyright e-design, Alexander Krause """ 9 | __license__ = "MIT" -------------------------------------------------------------------------------- /tools/inc/database/db_sqlite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | database helper 4 | """ 5 | __author__ = """Alexander Krause """ 6 | __date__ = "2015-08-08" 7 | __version__ = "0.1.0" 8 | __license__ = """MIT""" 9 | 10 | import sqlite3 11 | import os 12 | import logging 13 | import yaml 14 | 15 | import threading 16 | 17 | def dict_factory(cursor, row): 18 | d = {} 19 | for idx,col in enumerate(cursor.description): 20 | d[col[0]] = row[idx] 21 | return d 22 | 23 | class Database(object): 24 | def __init__(self,conf): 25 | pass 26 | -------------------------------------------------------------------------------- /lib/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | commmon helpers 4 | """ 5 | __author__ = """Alexander Krause """ 6 | __date__ = "2016-01-11" 7 | __version__ = "0.2.0" 8 | __credits__ = """Copyright e-design, Alexander Krause """ 9 | __license__ = "MIT" 10 | 11 | import struct 12 | 13 | MSG_RSP = 0 14 | MSG_LOGIN = 2 15 | MSG_PING = 6 16 | MSG_BRIDGE = 15 17 | MSG_HW = 20 18 | 19 | MSG_STATUS_OK = 200 20 | 21 | 22 | ProtocolHeader = struct.Struct("!BHH") 23 | 24 | def ArgsToBuffer(*args): 25 | # Convert params to string and join using \0 26 | return "\0".join(map(str, args)) 27 | 28 | def BufferToArgs(buff): 29 | return (buff.decode('ascii')).split("\0") 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyblynk - Blynk helpers for Python 2 | 3 | 4 | So this is just another Python implementation of the Blynk service ( http://blynk.cc ). 5 | 6 | I plan to also add server functionalty to implement a private cloud service to be indedepent from cloud.blynk.cc . 7 | 8 | ## example 9 | There is a simple example which connects to the blynk service and prints out detailed frame info: 10 | 11 | ``` 12 | erazor@s9 ~/d/p/t/examples> python2 hw.py 13 | Auth successfull 14 | (20, 36, 14) 15 | ('OnPinMode', 0, 'out') 16 | ('OnPinMode', 2, 'out') 17 | (20, 46, 4) 18 | ('OnVirtualRead', 1) 19 | (20, 47, 4) 20 | ('OnVirtualRead', 1) 21 | (20, 48, 4) 22 | ('OnVirtualRead', 1) 23 | (20, 49, 4) 24 | ``` 25 | 26 | For a custom implementation you only need to overload **lib.hm.Hardware** . 27 | -------------------------------------------------------------------------------- /examples/hw.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | example hardware 4 | """ 5 | __author__ = """Alexander Krause """ 6 | __date__ = "2016-01-11" 7 | __version__ = "0.2.0" 8 | __credits__ = """Copyright e-design, Alexander Krause """ 9 | __license__ = "MIT" 10 | 11 | 12 | import sys 13 | import os 14 | sys.path.append( 15 | os.path.join( 16 | os.path.dirname(__file__), 17 | '..' 18 | ) 19 | ) 20 | 21 | TOKEN = '' 22 | 23 | import lib.hw as blynk_hw 24 | import lib.client as blynk_client 25 | 26 | class myHardware(blynk_hw.Hardware): 27 | """ 28 | you'll probably have to overload the On* calls, 29 | see lib/hw.py 30 | """ 31 | pass 32 | 33 | cConnection=blynk_client.TCP_Client() 34 | if not cConnection.connect(): 35 | print('Unable to connect') 36 | sys.exit(-1) 37 | 38 | if not cConnection.auth(TOKEN): 39 | print('Unable to auth') 40 | 41 | cHardware=myHardware(cConnection) 42 | 43 | try: 44 | while True: 45 | cHardware.manage() 46 | except KeyboardInterrupt: 47 | raise 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexander Krause 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/server/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Client connection classes 4 | 5 | """ 6 | __author__ = """Alexander Krause """ 7 | __date__ = "2015-08-08" 8 | __version__ = "0.1.0" 9 | __license__ = "MIT" 10 | 11 | def createFromConf(conf,storage=None): 12 | if conf['type']=='tcp': 13 | return TCP_Server(conf['conf'],storage) 14 | 15 | class Server(object): 16 | conf=None 17 | Storage=None 18 | Connections=None 19 | 20 | def __init__(self,config,storage): 21 | self.conf=config 22 | self.Storage=storage 23 | self.Connections={} 24 | 25 | def run(self): 26 | pass 27 | 28 | def stop(self): 29 | pass 30 | 31 | class TCP_Server(Server): 32 | Server=None 33 | 34 | def run(self): 35 | import SocketServer 36 | class BlynkUserConnection(SocketServer.BaseRequestHandler): 37 | def handle(self): 38 | if not self.client_address[0] in self.Server.Connections: 39 | self.Server.Connections[self.client_address[0]]={} 40 | 41 | if len(self.Server.Connections[self.client_address[0]]) < self.Server.conf['max_user_connections']: 42 | 43 | self.Server.Connections[self.client_address[0]][self.client_address[1]]=self 44 | 45 | # self.request is the TCP socket connected to the client 46 | self.data = self.request.recv(1024).strip() 47 | print "{} wrote:".format(self.client_address[0]) 48 | print self.data 49 | # just send back the same data, but upper-cased 50 | self.request.sendall(self.data.upper()) 51 | 52 | def finish(self): 53 | if (self.client_address[0] in self.Server.Connections) and \ 54 | (self.client_address[1] in self.Server.Connections[self.client_address[0]]): 55 | 56 | del self.Server.Connections[self.client_address[0]][self.client_address[1]] 57 | 58 | return SocketServer.BaseRequestHandler.finish(self) 59 | 60 | def close(self): 61 | pass 62 | 63 | 64 | BlynkUserConnection.Server=self 65 | self.Server=SocketServer.TCPServer( 66 | (self.conf['net'], self.conf['port']), 67 | BlynkUserConnection 68 | ) 69 | self.Server.serve_forever() 70 | 71 | def close(self): 72 | for ip in self.Connections: 73 | for port in self.Connections[ip]: 74 | self.Connections[ip][port].close() 75 | 76 | -------------------------------------------------------------------------------- /tools/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __author__ = """Alexander Krause """ 4 | __date__ = "2015-08-08" 5 | __version__ = "0.1.0" 6 | __license__ = "MIT" 7 | 8 | import os 9 | import getopt 10 | import sys 11 | import pwd 12 | import grp 13 | 14 | sys.path.append( 15 | os.path.join( 16 | os.path.dirname(__file__), 17 | 'inc' 18 | ) 19 | ) 20 | sys.path.append( 21 | os.path.join( 22 | os.path.dirname(__file__), 23 | '..' 24 | ) 25 | ) 26 | 27 | """parse arguments""" 28 | import argparse 29 | parser = argparse.ArgumentParser( 30 | description='blynkD - Blynk (http://blynk.cc) service written in python' 31 | ) 32 | parser.add_argument( 33 | '-c','--config', type=str, 34 | help='configuration file if not given it tries to find files in this order: '+ 35 | '~/.config/blynkd.conf /etc/blynkd.conf ./default.conf' 36 | ) 37 | parser.add_argument( 38 | '-v','--version', action='version', version='%(prog)s '+__version__ 39 | ) 40 | parser.add_argument( 41 | '-l','--logger', type=str,default='default', 42 | help='use a specific logger (has to be configured in config)' 43 | ) 44 | parser.add_argument( 45 | '-u','--user', type=str, default=None, 46 | help='setuid to user' 47 | ) 48 | parser.add_argument( 49 | '-g','--group', type=str, default=None, 50 | help='setgid to group' 51 | ) 52 | parser.add_argument( 53 | '-d','--dump-config', action='store_true', 54 | help='test and dump config' 55 | ) 56 | 57 | args = parser.parse_args() 58 | 59 | """read configs""" 60 | import yaml 61 | 62 | cDir=os.path.dirname(__file__) 63 | cfg_file_list=[ 64 | os.path.join(cDir,'default.conf'), 65 | '/etc/blynkd.conf', 66 | os.path.expanduser('~/.config/blynkd.conf') 67 | ] 68 | config={} 69 | for cFile in cfg_file_list: 70 | try: 71 | config.update( 72 | yaml.load(open(cFile)) 73 | ) 74 | 75 | except Exception: 76 | pass 77 | 78 | if args.dump_config: 79 | import pprint 80 | pp=pprint.PrettyPrinter() 81 | pp.pprint(config) 82 | sys.exit(0) 83 | 84 | """setup logger""" 85 | import logging 86 | import logging.config 87 | 88 | 89 | if 'logging' in config: 90 | logging.config.dictConfig(config['logging']) 91 | 92 | if args.logger in config['logging']['loggers']: 93 | logging.getLogger(args.logger) 94 | else: 95 | logging.warning('Logger "%s" not configured' % args.logger) 96 | 97 | """user/group change""" 98 | if args.group != None: 99 | logging.info("Changing to group %s:%i"%(args.group,grp.getgrnam(args.group).gr_gid)) 100 | os.setgid(grp.getgrnam(args.group).gr_gid) 101 | 102 | if args.user != None: 103 | logging.info("Changing to user %s:%i"%(args.user,pwd.getpwnam(args.user).pw_uid)) 104 | os.setuid(pwd.getpwnam(args.user).pw_uid) 105 | 106 | """main part""" 107 | import database 108 | import lib.server as blynk_server 109 | cServer=blynk_server.createFromConf( 110 | config['server'], 111 | database.createFromConf(config['database']) 112 | ) 113 | 114 | try: 115 | cServer.run() 116 | 117 | except KeyboardInterrupt: 118 | pass 119 | 120 | cServer.stop() 121 | 122 | -------------------------------------------------------------------------------- /lib/hw.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | hardware class for overloading 4 | """ 5 | __author__ = """Alexander Krause """ 6 | __date__ = "2016-01-11" 7 | __version__ = "0.2.0" 8 | __credits__ = """Copyright e-design, Alexander Krause """ 9 | __license__ = "MIT" 10 | 11 | from . import common 12 | 13 | class Hardware(object): 14 | _Media=None 15 | 16 | def __init__(self,media): 17 | self._Media=media 18 | 19 | 20 | def manage(self): 21 | if self._Media: 22 | #print('manage') 23 | self._Media.keepConnection() 24 | rx_frame=self._Media.rxFrame() 25 | 26 | if rx_frame: 27 | print(rx_frame) 28 | if (rx_frame[0] == common.MSG_HW) or (rx_frame[0] == common.MSG_BRIDGE): 29 | data = self._Media.rx(rx_frame[2]) 30 | params=common.BufferToArgs(data) 31 | cmd=params.pop(0) 32 | self.OnMessage_HW( 33 | cmd, 34 | params 35 | ) 36 | self._Media.txFrame(common.MSG_RSP,common.MSG_STATUS_OK) 37 | else: 38 | self.OnMessage_Unknown(rx_frame[0],rx_frame[2]) 39 | 40 | def OnMessage_HW(self,cmd,params): 41 | if cmd=='info': 42 | self.OnHW_info() 43 | elif cmd=='pm': 44 | pairs=zip(params[0::2], params[1::2]) 45 | for (pin, mode) in pairs: 46 | pin=int(pin) 47 | self.OnPinMode(pin,mode) 48 | elif cmd=='dw': 49 | pin = int(params.pop(0)) 50 | val = params.pop(0) 51 | self.OnDigitalWrite(pin,val) 52 | elif cmd=='aw': 53 | pin = int(params.pop(0)) 54 | val = params.pop(0) 55 | self.OnAnalogWrite(pin,val) 56 | elif cmd=='dr': 57 | pin = int(params.pop(0)) 58 | val=self.OnDigitalRead(pin) 59 | 60 | self._Media.txFrameData( 61 | common.MSG_HW, 62 | common.ArgsToData( 63 | 'dw', 64 | pin, 65 | val 66 | ) 67 | ) 68 | elif cmd=='ar': 69 | pin = int(params.pop(0)) 70 | val=self.OnAnalogRead(pin) 71 | 72 | self._Media.txFrameData( 73 | common.MSG_HW, 74 | common.ArgsToBuffer( 75 | 'aw', 76 | pin, 77 | val 78 | ) 79 | ) 80 | elif cmd=='vw': 81 | pin = int(params.pop(0)) 82 | val = params.pop(0) 83 | self.OnVirtualWrite(pin,val) 84 | elif cmd=='vr': 85 | pin = int(params.pop(0)) 86 | val=self.OnVirtualRead(pin) 87 | 88 | self._Media.txFrameData( 89 | common.MSG_HW, 90 | common.ArgsToBuffer( 91 | 'vw', 92 | pin, 93 | val 94 | ) 95 | ) 96 | else: 97 | print("Unknown HW-Command %s"%cmd) 98 | 99 | def OnHW_info(self): 100 | pass 101 | 102 | def OnPinMode(self,pin,mode): 103 | print('OnPinMode',pin,mode) 104 | 105 | def OnDigitalWrite(self,pin,val): 106 | print('OnDigialWrite',pin,val) 107 | 108 | def OnAnalogWrite(self,pin,val): 109 | print('OnAnalogWrite',pin,val) 110 | 111 | def OnDigitalRead(self,pin): 112 | print('OnDigialRead',pin) 113 | return 0 114 | 115 | def OnAnalogRead(self,pin): 116 | print('OnAnalogRead',pin) 117 | return 0 118 | 119 | def OnVirtualWrite(self,pin,val): 120 | print('OnVirtualWrite',pin,val) 121 | 122 | def OnVirtualRead(self,pin): 123 | print('OnVirtualRead',pin) 124 | return 0 125 | 126 | def OnMessage_Unknown(self,msg_type,data): 127 | pass 128 | 129 | def OnMessage_Ping(self,data): 130 | pass 131 | -------------------------------------------------------------------------------- /lib/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | client helpers 4 | """ 5 | __author__ = """Alexander Krause """ 6 | __date__ = "2016-07-05" 7 | __version__ = "0.2.1" 8 | __credits__ = """Copyright e-design, Alexander Krause """ 9 | __license__ = "MIT" 10 | 11 | import time 12 | import socket 13 | 14 | from . import common 15 | 16 | class TCP_Client(object): 17 | _Server=None 18 | _Port=None 19 | _Socket=None 20 | _MessageID=None 21 | _t_lastRX=None 22 | _lastToken=None 23 | 24 | t_Ping=5 25 | connected=False 26 | 27 | def __init__(self,server='blynk-cloud.com',port=8442): 28 | self._Server=server 29 | self._Port=port 30 | 31 | def connect(self,timeout=3): 32 | print('connected') 33 | self.close() 34 | self._MessageID=0 35 | self._Socket=socket.create_connection( 36 | (self._Server,self._Port), 37 | timeout 38 | ) 39 | self._Socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 40 | 41 | if self._Socket: 42 | self.connected=True 43 | return self._Socket 44 | 45 | def close(self): 46 | if self._Socket: 47 | self._Socket.close() 48 | self.connected=False 49 | 50 | 51 | def tx(self,data): 52 | #print('tx',data) 53 | if self._Socket: 54 | try: 55 | self._Socket.sendall(data) 56 | except Exception: 57 | self.connected=False 58 | 59 | def rx(self,length): 60 | if self._Socket: 61 | d = [] 62 | l = 0 63 | while l < length: 64 | r = '' 65 | try: 66 | r = self._Socket.recv(length-l) 67 | self._t_lastRX=time.time() 68 | except socket.timeout: 69 | #print('rx-timeout') 70 | return '' 71 | except Exception as e: 72 | print('rx exception',str(e)) 73 | self.connected=False 74 | return '' 75 | if not r: 76 | self.connected=False 77 | return '' 78 | d.append(r) 79 | #print(d) 80 | l = l + len(r) 81 | 82 | ret=bytes() 83 | for cluster in d: 84 | ret=ret+cluster 85 | return ret 86 | 87 | def rxFrame(self): 88 | response=self.rx(common.ProtocolHeader.size) 89 | if response: 90 | return common.ProtocolHeader.unpack(response) 91 | 92 | def txFrame(self,msg_type,data): 93 | self.tx( 94 | common.ProtocolHeader.pack( 95 | msg_type, 96 | self.newMessageID(), 97 | data 98 | ) 99 | ) 100 | def txFrameData(self,msg_type,data): 101 | self.tx( 102 | common.ProtocolHeader.pack( 103 | msg_type, 104 | self.newMessageID(), 105 | len(data) 106 | )+data.encode('ascii') 107 | ) 108 | 109 | def newMessageID(self): 110 | self._MessageID=self._MessageID+1 111 | return self._MessageID 112 | 113 | def auth(self,token=None): 114 | if not token and self._lastToken: 115 | token=self._lastToken 116 | elif token: 117 | self._lastToken=token 118 | else: 119 | return False 120 | 121 | self.txFrame(common.MSG_LOGIN,len(token)) 122 | self.tx(str(token).encode('ascii')) 123 | response=self.rxFrame() 124 | if response: 125 | msg_type, msg_id, msg_status = response 126 | 127 | if (msg_status==common.MSG_STATUS_OK): 128 | print("Auth successfull") 129 | return True 130 | 131 | def Ping(self): 132 | print("Ping...") 133 | self.txFrame(common.MSG_PING,0) 134 | rx_frame=self.rxFrame() 135 | if rx_frame and \ 136 | (rx_frame[0]==common.MSG_RSP) and \ 137 | (rx_frame[1]==self._MessageID) and \ 138 | (rx_frame[2]==common.MSG_STATUS_OK): 139 | print("...Pong") 140 | return True 141 | 142 | def keepConnection(self): 143 | if not self.connected: 144 | if self.connect() and self.auth(): 145 | return True 146 | else: 147 | time.sleep(1) 148 | return False 149 | if (self._t_lastRX+self.t_Ping)