├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── scripts ├── refresh_session.sh ├── status.sh └── unlock.sh └── src ├── config.py ├── core ├── operation.py ├── session.py └── status.py ├── entrypoints ├── refresh_session.py ├── status.py └── unlock.py └── utils └── constraints.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Amagi_Yukisaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jLock 2 | 3 | Python Script to Open SJTU Dormitory Smart Lock 4 | 5 | 可以解锁交大智能寝室门锁的 Python 代码 6 | 7 | 本项目旨在提供一个思路原形 以便大家后续开发接入 iOS 自动化/米家/Apple Homekit 8 | 9 | ## 使用教程 10 | 11 | ### 快速启动本项目 12 | 13 | **推荐 Python 版本: 3.9-3.10** 14 | 15 | #### 安装依赖 16 | 17 | ```plain 18 | pysjtu 19 | requests 20 | ``` 21 | 22 | 可通过 `python3 -m pip install -r ./requirements.txt` 一键安装 23 | 24 | #### 本地配置 25 | 26 | 修改`src/config.py` 27 | 28 | ```python 29 | JACCOUNT_USERNAME = 'Amagi_Yukisaki' # 这里填入你的jAccount用户名 不含'@sjtu.edu.cn' 30 | JACCOUNT_PASSWORD64 = 'PASSW0RD' # 这里填入你的jAccount密码的base64编码 31 | ROOM_ID = '00112233445566778899aabbccddeeff' # 扫描你的门上的二维码 识别结果'roomid='后面的字符串,这里扫描结果会是40位,需要删除后8位 32 | ``` 33 | 34 | **密码 base64 编码获取方式** 35 | 36 | ```plain 37 | (base) Amagi@iMacPro ~ $ python3 38 | Python 3.9.12 (main, Apr 5 2022, 01:53:17) 39 | [Clang 12.0.0 ] :: Anaconda, Inc. on darwin 40 | Type "help", "copyright", "credits" or "license" for more information. 41 | >>> import base64 42 | >>> base64.b64encode(b'PASSW0RD') 43 | b'UEFTU1cwUkQ=' 44 | ``` 45 | 46 | **ROOM_ID 获取方式** 47 | 48 | 扫描门上二维码,复制`roomid=`后的字符串,**删去最后 8 位**。 49 | 50 | ### 运行 51 | 52 | `python3 ./src/unlock.py` 53 | 54 | ### 远程部署(基于 Shell) 55 | 56 | 你需要的物品 57 | 58 | - 一台可以 ssh 远程访问的服务器 59 | - 一个可以远程执行 ssh 命令的自动化工具(例: iOS 快捷指令) 60 | 61 | 推荐使用`venv`为项目创建虚拟环境,并避免对同一服务器上其他应用的影响 62 | 63 | **所有命令默认运行在项目根目录下** 64 | 65 | #### 创建环境 66 | 67 | ##### 安装 venv 68 | 69 | ```shell 70 | sudo apt install python3-venv # for Debian and Ubuntu 71 | sudo yum install python3-virtualenv # for CentOS and Fedora 72 | sudo pacman -S python-virtualenv # for Arch Linux and Manjaro 73 | ``` 74 | 75 | ##### 创建虚拟环境 76 | 77 | ```shell 78 | python3 -m venv venv 79 | ``` 80 | 81 | 此操作将在当前目录下创建名为`venv`的文件夹和虚拟环境 82 | 83 | ##### 安装依赖 84 | 85 | ```shell 86 | ./venv/bin/activate 87 | pip install -r ./requirements.txt -i https://mirror.sjtu.edu.cn/pypi/web/simple 88 | ``` 89 | 90 | #### 修改配置 91 | 92 | ##### 修改`src/config.py` 93 | 94 | 同[本地配置](####本地配置) 95 | 96 | ##### 修改 `scripts/*.sh` 97 | 98 | 将 99 | 100 | ```shell 101 | cd /home/automaton/jLock # your project directory 102 | ``` 103 | 104 | 修改为你项目根目录的**绝对路径** 105 | 106 | #### 运行 107 | 108 | 调用脚本 109 | 110 | ```shell 111 | /home/automaton/jLock/scripts/[unlock/status/refresh_session.sh] 112 | ``` 113 | 114 | 即可 115 | 116 | ##### 脚本作用 117 | 118 | - `unlock.sh`: 开锁 119 | - `status.sh`: 检查锁的状态,返回`0`表示开启,`1` 表示关闭(仅包含本地脚本开锁的状态,也就是说,如果通过其他方式,比如扫码开锁,这里仍会显示为锁定) 120 | - `refresh_session.sh`: 强行刷新登录状态,可加入 `crontab` 定时任务用于加快每次开锁速度 121 | 122 | ### 远程部署(基于 Home Assistant) 123 | 124 | 本部分仍待完善,这里先简单贴一个配置文件 125 | 126 | ```yaml 127 | switch: 128 | - platform: command_line 129 | scan_interval: 0.5 130 | switches: 131 | door_lock: 132 | command_state: /home/automaton/jLock/script/status.sh 133 | command_on: /home/automaton/jLock/script/unlock.sh 134 | unique_id: io.yukisaki.jlock.whateveryoulike 135 | 136 | lock: 137 | - platform: template 138 | name: Door Lock 139 | value_template: "{{ is_state('switch.door_lock', 'off') }}" 140 | unlock: 141 | service: switch.turn_on 142 | target: 143 | entity_id: switch.door_lock 144 | lock: 145 | service: switch.turn_off 146 | target: 147 | entity_id: switch.door_lock 148 | ``` 149 | 150 | ## 二次开发 151 | 152 | 鼓励大家利用本项目二次开发 将交大智能门锁接入各大智能家居平台 153 | 154 | ## 鸣谢 155 | 156 | [@PhotonQuantum](https://github.com/PhotonQuantum) for [pysjtu](https://github.com/PhotonQuantum/pysjtu) 157 | 158 | ## 许可证 159 | 160 | MIT 许可 161 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pysjtu 2 | requests -------------------------------------------------------------------------------- /scripts/refresh_session.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /Users/Amagi/Desktop/jLock # your project directory 4 | source ./venv/bin/activate # venv 5 | 6 | unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy # unset proxies 7 | python3 src/entrypoints/refresh_session.py 8 | -------------------------------------------------------------------------------- /scripts/status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /Users/Amagi/Desktop/jLock # your project directory 4 | source ./venv/bin/activate # venv 5 | 6 | unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy # unset proxies 7 | python3 src/entrypoints/status.py 8 | -------------------------------------------------------------------------------- /scripts/unlock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | cd /home/automaton/jLock # your project directory 4 | source ./venv/bin/activate # venv 5 | 6 | unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy # unset proxies 7 | python3 src/entrypoints/unlock.py 8 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | JACCOUNT_USERNAME = 'Amagi_Yukisaki' # Your jAccount username here, for example '[username]@sjtu.edu.cn' 2 | JACCOUNT_PASSWORD64 = b'UEFTU1cwUkQ=' # your base64 encoded jAccount password here, get it by base64.b64encode(b'PASSW0RD') 3 | ROOM_ID = '00112233445566778899aabbccddeeff' # 32 hexadecimal bits, get it by scanning the QR code on your door, copying the url and removing the last 8 bits 4 | 5 | REFRESH_SESSION_INTERVAL = int(60 * 60 * 1e9) # refresh every hour 6 | RELOCK_INTERVAL = int(6 * 1e9) # relock after 6 seconds 7 | 8 | SESSION_FILE_PATH = 'jLock.session' 9 | SESSION_TIME_FILE_PATH = 'session_refreshed_time.txt' 10 | OPERATION_TIME_FILE_PATH = 'last_unlocked_time.txt' 11 | 12 | LOG_LEVEL = 'INFO' -------------------------------------------------------------------------------- /src/core/operation.py: -------------------------------------------------------------------------------- 1 | import requests, logging, time 2 | 3 | def unlock_door(room_id: str, user_agent: str, referer: str, url: str, cookies: dict, operation_time_file_path: str): 4 | params = { 'roomid': room_id } 5 | headers = { 6 | 'User-Agent': user_agent, 7 | 'Referer': referer, 8 | } 9 | 10 | logging.info('Opening Door with ID {}'.format(room_id)) 11 | requests.get(url, params=params, cookies=cookies, headers=headers) 12 | logging.info('Door should be Opened') 13 | 14 | logging.info('Writing Operation Record') 15 | current_time = time.time_ns() 16 | with open(operation_time_file_path, 'w') as f: 17 | f.write(str(current_time)) -------------------------------------------------------------------------------- /src/core/session.py: -------------------------------------------------------------------------------- 1 | from struct import pack 2 | from time import time 3 | import time, logging, base64 4 | from xmlrpc.client import boolean 5 | import pysjtu 6 | 7 | def refresh_session(username: str, password64: bytes, refresh_interval: int, session_file_path: str, session_time_file_path: str, force_refresh: boolean = False): 8 | password = str(base64.b64decode(password64), encoding='utf-8') 9 | current_time = time.time_ns() 10 | logging.info('Current time is {} (unixNano)'.format(current_time)) 11 | last_time = -1 12 | try: 13 | with open(session_time_file_path, 'r') as f: 14 | last_time = int(f.read()) 15 | except: 16 | pass 17 | logging.info('Last Session Was Created At {} (unixNano)'.format(last_time)) 18 | 19 | session = pysjtu.Session() 20 | if force_refresh or current_time - last_time >= refresh_interval: 21 | logging.info('{}, Creating New Session'.format('Force Refresh' if force_refresh else 'Last Session Expired')) 22 | logging.info('Logging in JAccount with username {}'.format(username)) 23 | session.login(username=username, password=password) 24 | logging.info('Log in Successfully!') 25 | session.get('https://door.sjtu.edu.cn') 26 | logging.info('Dumping Current Session') 27 | session.dump(session_file_path) 28 | with open(session_time_file_path, 'w') as f: 29 | f.write(str(current_time)) 30 | else: 31 | logging.info('Loading Last Session') 32 | session.load(session_file_path) 33 | return session 34 | 35 | 36 | def get_cookies(username: str, password64: bytes, refresh_interval: int, session_file_path: str, session_time_file_path: str): 37 | session = refresh_session(username=username, password64=password64, refresh_interval=refresh_interval, session_file_path=session_file_path, session_time_file_path=session_time_file_path) 38 | 39 | logging.info('Collecting Cookies') 40 | cookies = {} 41 | for cookie in session.cookies.jar: 42 | if cookie.domain == 'door.sjtu.edu.cn': 43 | cookies[cookie.name] = cookie.value 44 | logging.info('Cookies Coollected Successfully!') 45 | 46 | return cookies 47 | -------------------------------------------------------------------------------- /src/core/status.py: -------------------------------------------------------------------------------- 1 | import time, logging 2 | 3 | def get_door_status(operation_time_file_path: str, relock_interval: int): # 1 for locked, 0 for unlocked 4 | current_time = time.time_ns() 5 | last_time = -1 6 | 7 | try: 8 | with open(operation_time_file_path, 'r') as f: 9 | last_time = int(f.read()) 10 | except: 11 | pass 12 | 13 | ret = int(current_time - last_time > relock_interval) 14 | 15 | return ret -------------------------------------------------------------------------------- /src/entrypoints/refresh_session.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('') 3 | 4 | import src.core.session as session 5 | import src.config as config 6 | 7 | import logging 8 | logging.basicConfig(level=config.LOG_LEVEL) 9 | 10 | session.refresh_session(username=config.JACCOUNT_USERNAME, 11 | password64=config.JACCOUNT_PASSWORD64, 12 | refresh_interval=config.REFRESH_SESSION_INTERVAL, 13 | session_file_path=config.SESSION_FILE_PATH, 14 | session_time_file_path=config.SESSION_TIME_FILE_PATH, 15 | force_refresh=True) 16 | -------------------------------------------------------------------------------- /src/entrypoints/status.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('') 3 | 4 | import src.core.status as status 5 | import src.config as config 6 | 7 | import logging 8 | logging.basicConfig(level=config.LOG_LEVEL) 9 | 10 | door_status = status.get_door_status(operation_time_file_path=config.OPERATION_TIME_FILE_PATH, 11 | relock_interval=config.RELOCK_INTERVAL) 12 | exit(door_status) -------------------------------------------------------------------------------- /src/entrypoints/unlock.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('') 3 | 4 | import src.utils.constraints as constraints 5 | import src.core.operation as operation 6 | import src.core.session as session 7 | import src.config as config 8 | 9 | import logging 10 | logging.basicConfig(level=config.LOG_LEVEL) 11 | 12 | cookies = session.get_cookies(username=config.JACCOUNT_USERNAME, 13 | password64=config.JACCOUNT_PASSWORD64, 14 | refresh_interval=config.REFRESH_SESSION_INTERVAL, 15 | session_file_path=config.SESSION_FILE_PATH, 16 | session_time_file_path=config.SESSION_TIME_FILE_PATH) 17 | 18 | 19 | operation.unlock_door(room_id=config.ROOM_ID, 20 | user_agent=constraints.REQUESTS_USER_AGENT, 21 | referer=constraints.REQUESTS_REFER_BASEURL.format(config.ROOM_ID), 22 | url=constraints.REQUESTS_URL, 23 | cookies=cookies, 24 | operation_time_file_path=config.OPERATION_TIME_FILE_PATH) 25 | -------------------------------------------------------------------------------- /src/utils/constraints.py: -------------------------------------------------------------------------------- 1 | REQUESTS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148; TaskCenterApp/3.2.6/iPhone 13 Pro Max/ScreenFringe' 2 | 3 | REQUESTS_REFER_BASEURL = 'https://door.sjtu.edu.cn/ui?roomid={}' 4 | 5 | REQUESTS_URL = 'https://door.sjtu.edu.cn/api/key' --------------------------------------------------------------------------------