You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
9.5 KiB
Python

#!/bin/python3
# Mi365 Scooter Library
# MiAuth - Authenticate and interact with Xiaomi devices over BLE
# Copyright (C) 2021 Daljeet Nandha + modified by @catSIXe
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
import sys
import json
import os
import argparse
import time
from bluepy import btle
from mim365mi.m365scooter import M365Scooter
from message import *
import struct
parser = argparse.ArgumentParser()
parser.add_argument("mac", help="mac address of target device")
parser.add_argument("-c", "--command", help="send command (w/o checksum) to uart and print reply")
parser.add_argument("-s", "--serial", action='store_true', help="retrieve serial number")
parser.add_argument("-v", "--version", action='store_true', help="retrieve firmware version")
parser.add_argument("-d", "--debug", action='store_true', help="activate debug log")
parser.add_argument("-r", "--register", action='store_true',
help="register with device / create token (caution: will lose bond to all other apps)")
parser.add_argument("-t", "--token_file", default="./mi_token",
help="path to mi token file (default: ./mi_token)")
args = parser.parse_args()
from miauth.mi.micrypto import MiCrypto
from paho.mqtt import client as mqtt_client
port = 1883
broker = '127.0.0.1'
topic = "m365/test/"
client_id = f'john-xina'
def connect_mqtt():
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected to MQTT Broker!")
else:
print("Failed to connect, return code %d\n", rc)
client = mqtt_client.Client(client_id)
client.username_pw_set('scooter', 'PASSWORD')
client.on_connect = on_connect
client.connect(broker, port)
return client
def main():
mqtt = connect_mqtt()
mc = M365Scooter(btle.Peripheral(), args.mac, debug=args.debug)
def lol(reg, payload):
payloadO = payload
payload = int.from_bytes(payload, signed=True, byteorder='little')
if reg == '23010d': #Motor phase A current
reg = 'esc_phase_a_current'
if reg == '23010e': #Motor phase B current
reg = 'esc_phase_b_current'
if reg == '23010f': #Motor phase C current
reg = 'esc_phase_c_current'
if reg == '230175': #drive mode
reg = 'drive_mode'
if reg == '230122': #batt percent
reg = 'bms_level'
if reg == '230150': #current
reg = 'esc_current_ma'
payload = payload * 10
if reg == '230148': #bms volt
reg = 'bms_volt'
payload = payload/1e2
if reg == '230147': #esc volt
reg = 'esc_volt'
payload = payload/1e2
if reg == '230126': #speed
reg = 'speed'
if reg == '230129': #Total mileage, m
reg = 'total_km'
payload = payload/1e3
if reg == '23012F': #Current mileage
reg = 'current_m'
if reg == '230132': #Total run time
reg = 'total_uptime'
if reg == '23013a': #triptime
reg = 'triptime1'
if reg == '23013e': #frame_temp
reg = 'frame_temp1'
payload = payload/1e1
if reg == '2301bb': #frame_temp
reg = 'frame_temp2'
payload = payload/1e1
if reg == '250140': # cell voltages
reg = 'bms_cell_voltages'
payload = []
for i in range(0, 10):
payload.append(int.from_bytes(payloadO[ (i*2) : (i*2)+2 ], signed=True, byteorder='little') /1e3)
payload = json.dumps(payload)
if reg == '25013b': # battery health
reg = 'bms_health'
if reg == '250130': # battery status
reg = 'bms_status'
if reg == '25011c': # Charge count
reg = 'bms_chargecount'
if reg == '25011b': # Charge full cycles
reg = 'bms_chargefullcount'
if reg == '250110': # Serial number
reg = 'bms_serial'
payload = payloadO.decode()
if reg == '250133': # bms current x10mA
reg = 'bms_current_ma'
payload = payload * 10
if reg == '250135': # bms temps high and low bytes, -20C offset
reg = 'bms_temp1'
payload = payloadO[ 1 ] - 20
mqtt.publish(topic + reg, payload)
reg = 'bms_temp2'
payload = payloadO[ 0 ] - 20
print(reg, payload)
mqtt.publish(topic + reg, payload)
mc.handleData(lol)
print("Connecting")
mc.connect()
if args.register:
print("Registering")
mc.register()
mc.save_token(args.token_file)
print("Saved token to:", args.token_file)
if not mc.token:
if not os.path.isfile(args.token_file):
sys.exit("""No authentication token found, register with '-r' or specify path to token file with '-t <path>'.Caution: After registration this device will lose coupling to all other apps (remove/add device in Mi Home app if needed). """)
print("Loading token from:", args.token_file)
mc.load_token(args.token_file)
print("Logging in...")
mc.login()
print("Retrieving serial number")
mc.comm_simplex("55aa032001 10 0e")
#print("Serial number:", resp.decode())
time.sleep(3)
#print("Retrieving firmware version")
#resp = mc.comm("55aa032001 1a 10")
#print("Firmware version:", f"{resp[0]}.{resp[1]}")
cmd = str(battery_info._raw_bytes.hex())
print("Sending command:", cmd)
def scooterPowerManagement(reboot=False):
mc.comm_simplex(Message() \
.set_direction(0x20) \
.set_read_write(ReadWrite.WRITE) \
.set_attribute(0x78 if reboot else 0x79) \
.set_payload(b'\x01\x00') \
.build()._raw_bytes.hex())
def on_message(client, userdata, msg):
print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic")
if msg.topic == topic +'cmds/poweroff':
scooterPowerManagement(True)
if msg.topic == topic +'cmds/reboot':
scooterPowerManagement(True)
mqtt.subscribe(topic+'cmds/#')
mqtt.on_message = on_message
mqtt.loop_start()
i = 0
while True:
i+=1
# ESC
if i % 10 == 0:
mc.comm_simplex('55AA 03 20 01 29 04') # Total mileage, m
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 2F 02') # Current mileage
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 32 04') # Total run time
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 34 04') # ? some run time
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 3A 04') # ? trip time
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 3B 04') # ? trip time
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 22 02') # BMS Percent
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 26 02') # Speed
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 47 02') # ESC supply voltage (measured by ESC)
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 48 02') # Battery voltage (from BMS)
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 50 02') # Battery current (from BMS)
time.sleep(0.05)
if i % 10 == 0:
mc.comm_simplex('55AA 03 20 01 75 02')
time.sleep(0.05)
# ESC Temperatures
if i % 10 == 0:
mc.comm_simplex('55AA 03 20 01 BB 02') #Frame temperature
time.sleep(0.05)
mc.comm_simplex('55AA 03 20 01 3E 02') # Frame temperature
time.sleep(0.05)
# BMS
if i % 10 == 0:
mc.comm_simplex('55AA 03 22 01 40 14') # Cell Voltage 1 - 10
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 35 02') # bTemperature1:bTemperature2, Deg C, 0 is -20
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 30 02') # Status
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 33 02') #Current, x10mA, positive - discharging, negative - charging
time.sleep(0.05)
if i % 180 == 0:
mc.comm_simplex('55AA 03 22 01 10 0E') #Serial number
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 17 02') #Firmware version
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 18 02') #Factory capacity
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 20 02') #Manufacture date
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 1B 02') #Charge full cycles
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 1C 02') #Charge count
time.sleep(0.05)
mc.comm_simplex('55AA 03 22 01 3B 02') #Health, %
time.sleep(0.05)
print("Disconnecting")
mc.disconnect()
if __name__ == "__main__":
main()