MQTT Connection #218
Replies: 16 comments 4 replies
|
Hi, I tried your code with SB1 and I receive data without need to trigger something with the App. The mqtt response contains pkcs12 data beside the certificates for the server connection. I assume this pkcs12 data is used for the payload encryption. The question is how to derive the private key and certificate from pkcs12 file without knowing its password? |
|
Looking at the timestamps of the messages, I get them in 60 second intervals, the normal rate as data is also available on power cloud. |
|
As far as i can tell, it is not encrypted. And i found the topic to send the update command, but i don't know what data i have to publish there.
|
|
You are probably right. After base64decode of the data field it shows the SN and couple of bytes, starting with a marker of 'ff09' like the BT hex fields. |
|
Hi @rschoebel BTW, I'm going to implement an MQTT client and a monitor tool into the library. My goal is to provide a standardized way to decode the binary payload in a consumable way to simplify mapping of fields to device model values. I think first step is to understand which data can be taken from the MQTT stats that are not available via Api yet. Those could also be merged with the HA integration. |
|
Hi, i had no time this weekend, to analyze the traffic. |
|
I had some time today to analyze my data, and the main message format seems identical to the Bluetooth one. and i think i found some data if the bytes[2:9] -> 'f8 01 03 01 0f 04 05' data types, current assumptions first byte of the data fields: i need to invest more time, and collect more data import base64
import binascii
coded_string = '''<input mqtt payload data>'''
bytes=base64.b64decode(coded_string)
def decode(pos):
start=int(pos)
section=hex(bytes[start])
length=bytes[start+1]
data=start + 2
next=data + length
print("Section: ",section)
#print("Start: ",start)
print("Length: ",hex(length))
#print("next: ",next)
#in the first section the length dose not include the section and length field
if (section == "0xff"):
next = next - 2
if(length > 1):
print("Data:\t ",binascii.hexlify(bytes[data:next], ' '))
else:
print("Data:\t ",int.from_bytes(bytes[data:next]))
return next
print("=========================")
pos = 0
while len(bytes) > pos:
if pos + 1 >= len(bytes):
print("Checksum: ", bytes[pos])
break
pos = decode(pos) |
|
And another update, i found a way to trigger mqtt updates. updated code below. next part wire data to sqlite, in try to identify more fields #!/usr/bin/env python
"""Example exec for mqtt."""
import asyncio
from datetime import datetime
import json
import logging
from pathlib import Path
from aiohttp import ClientSession
from api import api # pylint: disable=no-name-in-module
from api.apitypes import SolixParmType # pylint: disable=no-name-in-module
import common
import paho.mqtt.client as mqtt
import ssl
import time
import tempfile
import os
import re
_LOGGER: logging.Logger = logging.getLogger(__name__)
_LOGGER.addHandler(logging.StreamHandler())
# _LOGGER.setLevel(logging.DEBUG) # enable for detailed API output
CONSOLE: logging.Logger = common.CONSOLE
def _out(jsondata):
CONSOLE.info(json.dumps(jsondata, indent=2))
def on_connect(client, userdata, flags, rc):
"""Callback for when the client receives a CONNACK response from the server."""
if rc == 0:
print("Connected successfully to MQTT broker")
else:
print(f"Failed to connect, return code {rc}")
def on_message(client, userdata, msg):
"""Callback for when a PUBLISH message is received from the server."""
print(f"Received message: {msg.payload.decode()} on topic: {msg.topic}")
def on_disconnect(client, userdata, rc):
"""Callback for when the client disconnects from the server."""
print("Disconnected from MQTT broker")
def create_mqtt_client(cid,cert,key,ca):
"""Create and configure MQTT client with SSL/TLS certificates from strings."""
# Create client instance
client = mqtt.Client(client_id=cid, clean_session=True)
# Set callbacks
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect
# Create temporary files for certificates
ca_cert_file = None
client_cert_file = None
client_key_file = None
try:
# Create temporary file for CA certificate
ca_cert_file = tempfile.NamedTemporaryFile(mode='w', suffix='.crt', delete=False)
ca_cert_file.write(ca)
ca_cert_file.flush()
# Create temporary file for client certificate
client_cert_file = tempfile.NamedTemporaryFile(mode='w', suffix='.crt', delete=False)
client_cert_file.write(cert)
client_cert_file.flush()
# Create temporary file for client private key
client_key_file = tempfile.NamedTemporaryFile(mode='w', suffix='.key', delete=False)
client_key_file.write(key)
client_key_file.flush()
# Configure SSL/TLS using temporary files
client.tls_set(ca_certs=ca_cert_file.name,
certfile=client_cert_file.name,
keyfile=client_key_file.name,
cert_reqs=ssl.CERT_REQUIRED,
tls_version=ssl.PROTOCOL_TLS,
ciphers=None)
# Alternative method using SSL context (commented out):
# context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
# context.check_hostname = False
# context.load_verify_locations(ca_cert_file.name)
# context.load_cert_chain(client_cert_file.name, client_key_file.name)
# client.tls_set_context(context)
# Store temp file references for cleanup
client._temp_cert_files = [ca_cert_file.name, client_cert_file.name, client_key_file.name]
except Exception as e:
# Clean up temp files if there was an error
for temp_file in [ca_cert_file, client_cert_file, client_key_file]:
if temp_file and os.path.exists(temp_file.name):
temp_file.close()
os.unlink(temp_file.name)
raise e
finally:
# Close file handles (files remain on disk for SSL to use)
if ca_cert_file:
ca_cert_file.close()
if client_cert_file:
client_cert_file.close()
if client_key_file:
client_key_file.close()
return client
def cleanup_temp_files(client):
"""Clean up temporary certificate files."""
if hasattr(client, '_temp_cert_files'):
for temp_file_path in client._temp_cert_files:
try:
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
except Exception as e:
print(f"Warning: Could not delete temp file {temp_file_path}: {e}")
async def main() -> None:
"""Create the aiohttp session and run the example."""
CONSOLE.info("Testing Solix API:")
async with ClientSession() as websession:
myapi = api.AnkerSolixApi(
common.user(),
common.password(),
common.country(),
websession,
_LOGGER,
)
# show login response
new = await myapi.async_authenticate(
restart=True
) # enforce new login data from server
new = (
await myapi.async_authenticate()
) # receive new or load cached login data
if new:
CONSOLE.info("Received Login response:")
else:
CONSOLE.info("Cached Login response:")
_out(
myapi.apisession._login_response # noqa: SLF001
) # show used login response for API requests
mqtt = await myapi.apisession.request("post","v1/openapi/devicemanage/get_user_mqtt_info")
_out(
mqtt
)
await myapi.update_device_details()
CONSOLE.info("Device Overview:")
device = next(iter(myapi.devices))
TOPIC = f"dt/{mqtt['data']['app_name']}/{myapi.devices[device]['device_pn']}/{myapi.devices[device]['device_sn']}/param_info"
TOPIC_SEND = f"cmd/{mqtt['data']['app_name']}/{myapi.devices[device]['device_pn']}/{myapi.devices[device]['device_sn']}/req"
client = create_mqtt_client(mqtt["data"]["thing_name"],mqtt["data"]["certificate_pem"],mqtt["data"]["private_key"],mqtt["data"]["aws_root_ca1_pem"])
print(f"Connecting to MQTT broker at {mqtt['data']['endpoint_addr']}:8883")
client.connect(mqtt["data"]["endpoint_addr"], 8883, 20)
client.subscribe(TOPIC)
# Start the loop to process network traffic and callbacks
client.loop_start()
try:
while True:
f = {
"client_id": f"android-{mqtt['data']['app_name']}-{mqtt['data']['user_id']}-{mqtt['data']['certificate_id']}",
"device_sn": myapi.devices[device]['device_sn'],
"device_pn": myapi.devices[device]['device_pn'],
"data": "/wkfAAMADwBXoQEiogIBAaMFAywBAAD+BQPI17ZoIQ==",
"account_id": mqtt['data']['user_id'],
"timestamp": int(time.time())
}
data = '''
{{
"head": {{
"version": "1.0.0.1",
"client_id": "{client_id}",
"sess_id": "5681-3252",
"msg_seq": "1",
"seed": "1",
"timestamp": {timestamp},
"cmd_status": 2,
"cmd": 17,
"sign_code": 1,
"device_pn": "{device_pn}",
"device_sn": "{device_sn}"
}},
"payload": "{{\\"account_id\\": \\"{account_id}\\",\\"device_sn\\": \\"{device_sn}\\",\\"data\\": \\"{data}\\"}}
}}'''
client.publish(TOPIC_SEND, re.sub(r"[\n\t\s]*", "", data.format(**f)))
time.sleep(60)
except KeyboardInterrupt:
print("\nDisconnecting...")
client.loop_stop()
client.disconnect()
cleanup_temp_files(client)
# run async main
if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as err: # pylint: disable=broad-exception-caught # noqa: BLE001
CONSOLE.exception("%s: %s", type(err), err) |
|
I just uploaded the latest updates to main branch I can see if I can also build in the update topic payload that you found, so this might be helpful for general purpose. The question is whether you are able to sniff payloads for device control and apply settings. How is the payload structured and is it just sending the 'correct' field name with format and value like in the binary response? |
|
Here is an example how the decoding may look like. Any existing mappings will be added to the decoding output if the model is passed to the dataclass that structures the hexdata: If you have a hex string, you can simply do this for decoding. hexstr = "ff09......."
data = apitypes.DeviceHexData(model="A17X7",hexbytes=hexstr)
CONSOLE.info(str(data))
CONSOLE.info(data.decode())For general purpose use I'm just concerned how Anker implements changes. If they add fields in between which will shift the namings, the whole mapping definitions will become garbage... |
|
Thanks for the publish data. I'm trying to figure out how to compose it in general to add this to the mqtt module. f1 = {
"head": {
"version": "1.0.0.1",
"client_id": clientid,
"sess_id": "5681-3252", # => can this be fix, or can it be obtained from client connection?
"msg_seq": 1,
"seed": 1,
"timestamp": int(datetime.now().timestamp()),
"cmd_status": 2,
"cmd": 17,
"sign_code": 1,
"device_pn": devicePn,
"device_sn": deviceSn,
},
"payload": json.dumps({
"account_id": accountId,
"device_sn": deviceSn,
"data": "/wkfAAMADwBXoQEiogIBAaMFAywBAAD+BQPI17ZoIQ==",
},separators=(',', ':'))
}
client.publish(TOPIC_SEND, json.dumps(f1,separators=(',', ':')))But when I look to your hex data, I'm puzzeled what the fields actually mean. I guess the hex data must be composed too and base64 encoded. Any clue how to compose the field values? ------------------------------------ Header ------------------------------------
ff 09 : 2 Byte Anker Solix message marker (supposed 'ff 09')
1f 00 : 2 Byte total message length (31) in Bytes (Little Endian format)
03 00 0f: 3 Byte fixed message pattern (supposed `03 01 0f`)
00 57 : 2 Byte message type pattern (varies per device model and message type)
: 1 Byte optional message increment ( 0)
-- Fields --|- Value (Hex/Decode Options)---------------------------------------
Fld Len Typ uIntLe/var sIntLe floatLe dblLe/4int
a1 01 -- 22
└-> 1 unk 34 34
a2 02 01 01
└-> 2 ui 1 1
a3 05 03 2c:01:00:00
└-> 5 var 300;0 300 0.00 44;1;0;0
fe 05 03 c8:d7:b6:68
└-> 5 var -10296;26806 1756813256 6907609291015444544618496.00 200;215;182;104
--------------------------------------------------------------------------------The message pattern is slightly different, OK. But what does the a1-a3 field values mean? The fe field seems to be just a finish pattern which is also in other messages, but I have no clue whether that is fixed or what it means. however it seems to follow the common field structure with name, length, type and 4 bytes of value. |
|
as far as i can tell in the messages with the header 03:00:0f:00:57 and i had no problems leaving the msg_seq and sess_id fixed. message header i got till today from byte 4 [4] 0x03 -> seems fixed for packages with header 03:01:0f:04:05 |
|
OK, I uploaded latest updates. Further enhancements and fixed to the dataclasses for the MQTT hex data. They can now be used also to compose headers, fields etc. message, response = mqtt_session.publish(
deviceDict=device_selected,
hexbytes=mqtt_session.get_command_data(
command="update_trigger", parameters={"timeout": 120}
),
)
CONSOLE.info(f"Published message: {response!s}\n{message!s}")I kept the mqtt_session.get_command_data quite generic. So once new hexdata for other commands are known, they can be implemented accordingly. Here is an example how the hexdata is built, showing also different methods to build the data fields: if command == "update_trigger":
hexdata = DeviceHexData(msg_header=DeviceHexDataHeader(cmd_msg="0057"))
hexdata.update_field(DeviceHexDataField(hexbytes="a10122"))
hexdata.update_field(DeviceHexDataField(hexbytes="a2020101"))
hexdata.update_field(
DeviceHexDataField(
f_name=bytes.fromhex("a3"),
f_type=DeviceHexDataTypes.var.value,
f_value=int(parameters.get("timeout") or 60).to_bytes(
length=4, byteorder="little"
),
)
)
hexdata.add_timestamp_field()
if hexdata:
self._logger.info("Generated hexdata for device mqtt command '%s':\n%s", command, hexdata.hex(":"))
return hexdata.hex()Any value parameters could be passed with the mapping, and used properly in the get_command_data method. |
|
Making more progress on MQTT integration, but there is still lot to do on my list:
I will upload a new main branch update once I made for progress on the first part of the list. For the time being, field mapping documentation is easiest with output from the mqtt_monitor. The model, message pattern and clear description of required byte conversion must be provided for non basic field formats. Ideally with an example value if conversion factors are needed. |
|
Hi @rschoebel I'm done with most of the items I wanted to include and updated the main branch. In the future, I will see how to make use of the MQTT data and how to integrate them into the Api cache structures. |
|
Hi, and at this point in time, i think all change requests to the device are done via the api and not mqtt. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Hi,
i found some api endpoint to get certificates for the mqtt server,
my current problem is i can't trigger an update to receive new mqtt data.
but if i run this script and connect via the app on my smart phone i got some data :)
needs
pip install paho-mqttAll reactions