├── .gitignore ├── README.md ├── __init__.py ├── docker_run.sh ├── influxdb.py ├── lib ├── __init__.py ├── conf.py └── read.py ├── push_to_influxdb.sh ├── requirements.txt ├── setup.sh ├── start_web.sh ├── templates └── meters.html └── web.py /.gitignore: -------------------------------------------------------------------------------- 1 | lib/init.sh 2 | lib/forked/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | DLT645 批量抄表 3 | 基于: [meter-dlt645](https://github.com/glx-technologies/meter-dlt645) 4 | 5 | # 运行 6 | 编辑`lib/conf.py`:COM口、电表通信地址等 7 | 8 | ## Docker 9 | 10 | ./docker_run.sh # 可修改`custom_config`值自定义conf.py位置 11 | docker exec flask-dlt645 bash -c "cd /app && echo y | ./push_to_influxdb.sh" 12 | 13 | ![](https://i.imgur.com/t5iecct.png) 14 | 15 | ## 手动 16 | 17 | pip -r requirements.txt 18 | ./start_web.sh 19 | 20 | ![](https://imgur.com/frgWGHF.png) 21 | 22 | ## 写入InfluxDB数据库(可选) 23 | 24 | 1. 编辑以上conf文件 25 | 2. 26 | 27 | ./push_to_influxdb.sh # 发送当前度数 28 | 29 | 3. 生成报表 30 | 31 | from(bucket: "YOUR_BUCKET_NAME") 32 | |> range(start: v.timeRangeStart, stop: v.timeRangeStop) 33 | |> filter(fn: (r) => r["_measurement"] == "度数") 34 | |> filter(fn: (r) => r["_field"] == "当前") 35 | |> difference() 36 | 37 | ![](https://i.imgur.com/wwYoqzl.png) 38 | 39 | 使用Jupyter分析数据:https://nbviewer.jupyter.org/github/fzinfz/ipynb/tree/main/python/DB/influxdb-client.ipynb 40 | 41 | # 作为库使用 42 | Demo:[多表](https://nbviewer.jupyter.org/github/fzinfz/ipynb/blob/main/python/hw/power_meter_DLT645/multi.ipynb) | 43 | [单表](https://nbviewer.jupyter.org/github/fzinfz/ipynb/blob/main/python/hw/power_meter_DLT645/single.ipynb) 44 | 45 | # 参考资料 46 | 说明书范例:[威胜](http://www.wasion.com/UploadFiles/files/DTSD342DSSD342-5N5D5Z%E5%AF%BC%E8%BD%A8%E5%AE%89%E8%A3%85%E7%94%B5%E5%AD%90%E5%BC%8F%E5%A4%9A%E5%8A%9F%E8%83%BD%E7%94%B5%E8%83%BD%E8%A1%A8%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E%E4%B9%A6.pdf) 47 | 液晶全屏及显示说明: 第9页 48 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 3 | 4 | from lib.forked import * 5 | from lib.read import * 6 | from lib.conf import * 7 | 8 | devices = Meters(meter_list_str) 9 | 10 | addr_convert = lambda addr: [ int(s,16) for s in re.findall('..', addr) ] 11 | 12 | def iter_meters(): 13 | for d in devices.devices: 14 | addr = d[0] 15 | m = Meter(chn, addr_convert(addr), level=1, verbose=0) 16 | yield devices.df.loc[addr]['Tag'], m.read_meter() 17 | -------------------------------------------------------------------------------- /docker_run.sh: -------------------------------------------------------------------------------- 1 | custom_config=/data/conf/flask-DLT645/*.py # to replace lib/conf.py , ignore if not existing 2 | n=flask-dlt645 # container name 3 | 4 | . ./setup.sh 5 | 6 | select_file $custom_config 7 | custom_config=$selected_file 8 | 9 | select_file /dev/ttyUSB* 10 | dev_usb=$selected_file 11 | [ ! -c $dev_usb ] && exit_err "$dev_usb not found!" 12 | 13 | run "docker stop $n 2>/dev/null; docker rm $n 2>/dev/null" 14 | 15 | q "Port: (Default: 5000) " p 16 | 17 | q "Debug mode? ([n]/y) " m 18 | [[ $m =~ [Yy] ]] && FLASK_ENV=development || FLASK_ENV=production 19 | echo_debug "FLASK_ENV=$FLASK_ENV" 20 | 21 | s="docker run --name $n -d --restart unless-stopped \ 22 | --net host \ 23 | -e FLASK_DLT645_PORT=${p:-5000} \ 24 | -e FLASK_ENV=${FLASK_ENV} \ 25 | --device=$dev_usb:/dev/ttyUSB0 \ 26 | -v $PWD:/app \ 27 | " 28 | 29 | [ -n "$custom_config" ] && echo_debug "Loaded: $custom_config" && s="$s -v $custom_config:/app/lib/conf.py " 30 | 31 | s="$s fzinfz/tools:python3 bash -c 'cd /app && ./start_web.sh' " 32 | 33 | run "$s" 34 | run "docker logs -f $n" -------------------------------------------------------------------------------- /influxdb.py: -------------------------------------------------------------------------------- 1 | from __init__ import * 2 | 3 | from influxdb_client import InfluxDBClient, Point, WritePrecision 4 | from influxdb_client.client.write_api import SYNCHRONOUS 5 | 6 | print(f'{influxdb_url}, bucket={influxdb_bucket}, org={influxdb_org}') 7 | influxdb_client_obj = InfluxDBClient(url=influxdb_url, token=influxdb_token) 8 | 9 | write_api = influxdb_client_obj.write_api(write_options=SYNCHRONOUS) 10 | 11 | chn.open() 12 | 13 | def push(): 14 | sequence = [] 15 | for d in devices.devices: 16 | addr = d[0] 17 | m = Meter(chn, addr_convert(addr), level=1, verbose=0) 18 | meter_data = m.read_meter() 19 | s = f"度数,tag={devices.df.loc[addr]['Tag']} 当前={meter_data['电能-组合有功总-当前'][0]}" 20 | print(s) 21 | sequence.append(s) 22 | 23 | write_api.write(influxdb_bucket, influxdb_org, sequence) 24 | 25 | if __name__ == '__main__': 26 | push() -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) -------------------------------------------------------------------------------- /lib/conf.py: -------------------------------------------------------------------------------- 1 | # 1-Addr(非表号) 2-Tag 3..-可选自定义列\ 2 | 3 | meter_list_str = ''' 4 | 010128318569 表1 5 | # 001522454104 表2 6 | 000080853040 表3 7 | ''' 8 | 9 | from forked import dlt645 10 | chn=dlt645.Channel(port_id = '/dev/ttyUSB0', tmo_cnt = 10, wait_for_read = 0.5) 11 | 12 | # [可选] influxdb (OSS V2.0 tested) 13 | 14 | influxdb_url="http://192.168.1.72:8086" 15 | influxdb_token = "" 16 | influxdb_org = "" 17 | influxdb_bucket = "" 18 | -------------------------------------------------------------------------------- /lib/read.py: -------------------------------------------------------------------------------- 1 | import sys, re, os 2 | import pprint 3 | import pandas as pd 4 | from collections import ChainMap 5 | 6 | # (DI3~0, 整数位数,小数位数, 单位) 7 | properties_list = [ 8 | { 9 | '电能-组合有功总-当前': ('00 00 00 00', 6, 2, 'kWh'), 10 | }, 11 | { 12 | '功率-瞬时总有功': ('02 03 00 00', 2, 4, 'kW'), 13 | }, 14 | { 15 | '电能-组合有功总-上结算日': ('00 00 00 01', 6, 2, 'kWh'), 16 | 17 | 'A相电压': ('02 01 01 00', 3, 1, 'V'), 18 | 'A相电流': ('02 02 01 00', 3, 3, 'A'), 19 | 20 | '功率-一分钟有功总平均': ('02 80 00 03', 2, 4, 'kW'), 21 | '功率因数-总': ('02 06 00 00', 1, 3, ''), 22 | }, 23 | { 24 | '内部电池电压': ('02 80 00 08', 2, 2, 'V'), 25 | 26 | '日期': ('04 00 01 01', 6, 2, '年月日.星期'), 27 | '时间': ('04 00 01 02', 4, 2, 'hhmm.ss'), 28 | }, 29 | { 30 | 'B相电压': ('02 01 02 00', 3, 1, 'V'), 31 | 'C相电压': ('02 01 03 00', 3, 1, 'V'), 32 | 33 | 'B相电流': ('02 02 02 00', 3, 3, 'A'), 34 | 'C相电流': ('02 02 03 00', 3, 3, 'A'), 35 | 36 | '功率-瞬时A相有功': ('02 03 01 00', 2, 4, 'kW'), 37 | '功率-瞬时B相有功': ('02 03 02 00', 2, 4, 'kW'), 38 | '功率-瞬时C相有功': ('02 03 03 00', 2, 4, 'kW'), 39 | 40 | '功率-瞬时总视在': ('02 05 00 00', 2, 4, 'kVA'), 41 | 42 | '零线电流': ('02 80 00 01', 3, 3, 'A'), 43 | '频率': ('02 80 00 02', 2, 2, 'Hz'), 44 | 45 | '表内温度': ('02 80 00 07', 3, 1, '°C'), 46 | '内部电池工作时间': ('02 80 00 0A', 8, 0, '分'), 47 | 48 | '表号': ('04 00 04 02', 12, 0, '#'), 49 | '通信地址': ('04 00 04 01', 12, 0, '#'), 50 | 51 | '每月第1结算日': ('04 00 0B 01', 2, 2, '日.时'), 52 | '每月第2结算日': ('04 00 0B 02', 2, 2, '日.时'), 53 | '每月第3结算日': ('04 00 0B 03', 2, 2, '日.时'), 54 | 55 | '型号': ('04 00 04 0B', 20, 0, ''), 56 | 57 | '状态字1': ('04 00 05 01', 4, 0, 'int'), 58 | '状态字2': ('04 00 05 02', 4, 0, 'int'), 59 | '状态字3': ('04 00 05 03', 4, 0, 'int'), 60 | '状态字4': ('04 00 05 04', 4, 0, 'int'), 61 | '状态字5': ('04 00 05 05', 4, 0, 'int'), 62 | '状态字6': ('04 00 05 06', 4, 0, 'int'), 63 | '状态字7': ('04 00 05 07', 4, 0, 'int'), 64 | } 65 | ] 66 | 67 | 68 | class Meter: 69 | def __init__(self, chn, addr, level=1, verbose=0): 70 | self.chn = chn 71 | self.addr = addr 72 | self.properties = dict(ChainMap(*properties_list[:level])) 73 | self.verbose = verbose 74 | 75 | 76 | def get_data(self, item): 77 | 78 | d = self.properties[item] 79 | cmd = [ int(x, 16) for x in d[0].split(' ')[::-1] ] 80 | self.chn.encode(self.addr, 0x11, cmd) 81 | self.chn.xchg_data(self.verbose) 82 | payload = self.chn.rx_payload 83 | 84 | len_whole, len_decimal = d[1], d[2] 85 | len_payload = int( (len_whole + len_decimal) / 2 ) 86 | 87 | hex_str = ''.join([ "%02x" % x for x in payload[::-1][:len_payload] ]) 88 | try: 89 | value = int(hex_str) / pow(10, len_decimal) 90 | except ValueError: 91 | value = hex_str 92 | 93 | unit = d[3] 94 | return value, unit 95 | 96 | 97 | def read_meter(self): 98 | 99 | D = self.properties 100 | result = {} 101 | 102 | for item in D: 103 | result[item] = self.get_data(item) 104 | 105 | if len(D) == 1: return result 106 | 107 | for k, v in D.items(): 108 | unit = v[3] 109 | if unit == 'kW': 110 | result[k] = "{:,.2f}".format( result[k][0] * 1000 ) , 'W' 111 | if unit == '#': 112 | result[k] = "{0:0>12d}".format(int(result[k][0])), '' 113 | if unit == '分': 114 | result[k] = "{:,.2f}".format( result[k][0] / (60*24) / 365 ), '年' 115 | 116 | try: 117 | 118 | result['功率-A相'] = "{:,.2f}".format( result['A相电压'][0] * result['A相电流'][0] ) , 'W' 119 | result['电能:本周期'] = "{:,.2f}".format( result['电能-组合有功总-当前'][0] - result['电能-组合有功总-上结算日'][0] ) , 'kWh' 120 | 121 | result['日期时间'] = str(result['日期'][0] + 20000000).split('.')[0] + ' ' + \ 122 | ':'.join( re.findall('..', "{0:0>6d}".format(int(result['时间'][0]*100))) ), '' 123 | del result['日期'] 124 | del result['时间'] 125 | except: 126 | pass 127 | 128 | return result 129 | 130 | 131 | class Meters: 132 | 133 | def __init__(self, meter_list_str): 134 | _s = meter_list_str # defined in conf.py 135 | self.devices = [ re.findall('[^ ]+', line)[:2] 136 | for line in _s.strip().splitlines() if not line.startswith('#') ] 137 | self.df = pd.DataFrame(self.devices, columns =['Addr','Tag']).set_index('Addr') 138 | 139 | def read_meters(self, chn, level=2, verbose=0): 140 | meters = self.devices 141 | chn.open() 142 | print(chn.ser) 143 | # print('read_meters()', chn.ser.isOpen()) 144 | 145 | result = {} 146 | for meter in meters: 147 | print('\n', '='* 5, meter, '='* 5) 148 | addr_human = meter[0] 149 | addr = [ int(s,16) for s in re.findall('..', addr_human) ] 150 | m = Meter(chn, addr, level, verbose) 151 | rs = m.read_meter() 152 | pprint.pprint(rs) 153 | result[addr_human] = rs 154 | 155 | chn.close() 156 | return result 157 | -------------------------------------------------------------------------------- /push_to_influxdb.sh: -------------------------------------------------------------------------------- 1 | . ./setup.sh 2 | 3 | python influxdb.py 4 | 5 | which crontab &>/dev/null 6 | if [ $? -eq 0 ]; then 7 | echo -e '\n==run "crontab -e" to edit cron jobs==\n' 8 | crontab -l 9 | fi 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | pandas 3 | pyserial 4 | influxdb-client 5 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | if [ ! -f ./lib/forked/dlt645.py ]; then 2 | mkdir -p lib/forked 3 | wget https://raw.githubusercontent.com/glx-technologies/meter-dlt645/master/dlt645.py -P lib/forked 4 | fi 5 | 6 | if [ ! -f ./lib/init.sh ]; then 7 | wget https://raw.githubusercontent.com/fzinfz/scripts/master/linux/init.sh -P lib 8 | fi 9 | . ./lib/init.sh -------------------------------------------------------------------------------- /start_web.sh: -------------------------------------------------------------------------------- 1 | . ./setup.sh 2 | 3 | [ -z "$FLASK_DLT645_PORT" ] && export FLASK_DLT645_PORT=5000 4 | 5 | export PYTHONUNBUFFERED=1 6 | 7 | export FLASK_APP=web.py 8 | flask run --host=0.0.0.0 --port=$FLASK_DLT645_PORT --no-reload 9 | -------------------------------------------------------------------------------- /templates/meters.html: -------------------------------------------------------------------------------- 1 | 2 | 电表 3 | 4 | 5 | 31 | 32 | 33 |
34 |

{{ now }}

35 | 36 |
37 | 38 | {% for m in meters %} 39 |

40 | {{ m[0] }}: 41 | {{ m[1]['电能-组合有功总-当前'][0] }}度 42 |

43 | {% endfor %} -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | 3 | from flask import Flask 4 | app = Flask(__name__) 5 | 6 | # Folowing: https://flask.palletsprojects.com/en/1.1.x/patterns/streaming/#streaming-from-templates 7 | # but streaming not working on iPad 8 | 9 | from flask import Response 10 | def stream_template(template_name, **context): 11 | app.update_template_context(context) 12 | t = app.jinja_env.get_template(template_name) 13 | rv = t.stream(context) 14 | rv.enable_buffering(5) 15 | return rv 16 | 17 | from datetime import datetime 18 | import pytz 19 | tz = pytz.timezone('Asia/Shanghai') 20 | 21 | from flask import Response 22 | 23 | @app.route('/') 24 | @app.route('/meters/') 25 | def read(): 26 | 27 | chn.open() 28 | meters = iter_meters() 29 | 30 | now = datetime.now(tz).strftime("%Y-%m-%d %X") 31 | 32 | return Response(stream_template('meters.html', 33 | meters=meters, 34 | now=now 35 | )) 36 | --------------------------------------------------------------------------------