Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor icon indicating copy to clipboard operation
Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor copied to clipboard

Request new Vacuum Xiaomi s12

Open alejarvis opened this issue 1 year ago • 86 comments

Checklist

  • [X] I have updated the integration to the latest version available
  • [X] I have checked if the vacuum/platform is already requested
  • [ ] I have sent raw map file to piotr.machowski.dev [at] gmail.com (Retrieving map; please provide your GitHub username in the email)

What vacuum model do you want to be supported?

xiaomi.vacuum.b106eu

What is its name?

Xiaomi Robot Vacuum S12

Available APIs

  • [ ] xiaomi
  • [ ] viomi
  • [ ] roidmi
  • [ ] dreame

Errors shown in the HA logs (if applicable)

2023-08-19 05:08:13.598 ERROR (MainThread) [homeassistant.helpers.entity] Update for camera.xiaomi_cloud_map_extractor fails
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 699, in async_update_ha_state
    await self.async_device_update()
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 940, in async_device_update
    await hass.async_add_executor_job(self.update)
  File "/usr/local/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/xiaomi_cloud_map_extractor/camera.py", line 278, in update
    self._handle_map_data(map_name)
  File "/config/custom_components/xiaomi_cloud_map_extractor/camera.py", line 335, in _handle_map_data
    map_data, map_stored = self._device.get_map(map_name, self._colors, self._drawables, self._texts,
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/xiaomi_cloud_map_extractor/common/vacuum.py", line 27, in get_map
    response = self.get_raw_map_data(map_name)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/xiaomi_cloud_map_extractor/common/vacuum.py", line 45, in get_raw_map_data
    map_url = self.get_map_url(map_name)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/xiaomi_cloud_map_extractor/common/vacuum_v2.py", line 18, in get_map_url
    if api_response is None or "result" not in api_response or "url" not in api_response["result"]:
                                                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: argument of type 'NoneType' is not iterable

Other info

No response

alejarvis avatar Aug 19 '23 03:08 alejarvis

I'm joining the add request!

4ronos avatar Oct 14 '23 17:10 4ronos

@PiotrMachowski, please tell me if there is any reason why support for this vacuum cleaner is not yet available? (s12/s10) Is it a matter of time, or do mijia servers not provide a card for it?

4ronos avatar Oct 20 '23 14:10 4ronos

@4ronos it is not a simple process "just download a map image from a URL". API has to be found and map has to be parsed from a binary file that has an unknown structure. And I have a lot of other repositories to maintain. If you can find an already existing implementation (even not in python) then it would be much much easier for me.

PiotrMachowski avatar Oct 20 '23 15:10 PiotrMachowski

I need that too. Thanks.

yopami avatar Oct 22 '23 20:10 yopami

I need that too. Thanks.

nineteen0815 avatar Dec 07 '23 07:12 nineteen0815

Have the same model. Anything i can do to help to speed up the fix?

vadss avatar Dec 08 '23 10:12 vadss

Hi, same vacuum for me.

daco77 avatar Dec 14 '23 14:12 daco77

It's the same model as Xiaomi Robot Vacuum S10 B106GL, i don't think they changed something.

vadss avatar Dec 15 '23 08:12 vadss

Yeah, it would be great to have that integration available

casablancashub avatar Jan 14 '24 10:01 casablancashub

I have the same vacuum and I would like to help! I'm very interested supporting this model

Borty97 avatar Feb 25 '24 20:02 Borty97

I`ve done some research and succesfully downloaded map from xiaomi.vacuum.c103
I guess that would work with some newest models as well The url for obtaining map file is https://api.io.mi.com/app/v2/home/get_interim_file_url_pro Object name is {user_id}/{device_id}/0 I only have one map on my vacuum, later I will try to create another one and check if it possible to download map by {user_id}/{device_id}/{map_name}

It seems like map file is not in zlib/gz format, maybe it is encrypted. At that point I cannot say it exactly

maksp86 avatar Mar 01 '24 05:03 maksp86

I could hep too, I just received a xiaomi.vacuum.b106eu

r-jean-pierre avatar Mar 07 '24 11:03 r-jean-pierre

@maksp86 wich is the format of the payload? The URL is correct? I get {"code":0,"message":"auth err"} JSON response (without autentication 😓). How do you authenticate? Shall we add the {user_id}/{device_id}/{map_name} endpoint?

Borty97 avatar Mar 07 '24 12:03 Borty97

Update: I've got map decrypted by reverse engineering the mihome plugin for xiaomi.vacuum.c103 (i guess that would work not only for my vacuum). I will post some code later. Now I have only one thing (some weird serial number) that I cant get from api

Some summary: encryption algorithm is a AES (ECB mode), pkcs7 padding Key is generated from serial_num+owner_id+device_id

After decryption I got hex string, that represents some zlib inflated file, so I think map is in viomi format

maksp86 avatar Mar 07 '24 12:03 maksp86

This is a reverse-engeneered map encryption algorithm

from Crypto.Cipher import AES
from Crypto.Hash import MD5
from Crypto.Util.Padding import pad, unpad
import base64

isEncryptKeyTypeHex = True

def aesEncrypted(data, key: str):
    cipher = AES.new(key.encode("utf-8"), AES.MODE_ECB)

    encryptedData = cipher.encrypt(
        pad(data.encode("utf-8"), AES.block_size, 'pkcs7'))

    encryptedBase64Str = base64.b64encode(encryptedData).decode("utf-8")
    return encryptedBase64Str


def aesDecrypted(data, key: str):
    parsedKey = key.encode("utf-8")
    if isEncryptKeyTypeHex:
        parsedKey = bytes.fromhex(key)

    cipher = AES.new(parsedKey, AES.MODE_ECB)

    decryptedBytes = cipher.decrypt(base64.b64decode(data))
    decryptedData = unpad(decryptedBytes, AES.block_size, 'pkcs7')
    decryptedStr = decryptedData.decode("utf-8")
    return decryptedStr


def md5key(string: str, model: str, device_mac: str):
    pjstr = "".join(device_mac.lower().split(":"))

    tempModel = model.split('.')[-1]

    if len(tempModel) == 2:
        tempModel = "00" + tempModel
    elif len(tempModel) == 3:
        tempModel = "0" + tempModel

    tempKey = pjstr + tempModel
    aeskey = aesEncrypted(string, tempKey)

    temp = MD5.new(aeskey.encode('utf-8')).hexdigest()
    if isEncryptKeyTypeHex:
        return temp
    else:
        return temp[8:-8].upper()


def genMD5key(wifi_info_sn: str, owner_id: str, device_id: str, model: str, device_mac: str):
    arr = [wifi_info_sn, owner_id, device_id]
    tempString = '+'.join(arr)
    return md5key(tempString, model, device_mac)


def unGzipCommon(data: bytes, wifi_info_sn: str, owner_id: str, device_id: str, model: str, device_mac: str):
    tempKey = genMD5key(wifi_info_sn, owner_id, device_id, model, device_mac)
    base64map = base64.b64encode(data)

    tempString = aesDecrypted(base64map, tempKey)
    return tempString

P.S functions naming was taken from original code Usage:

map_file = some_func_to_download_map() -> bytes
wifi_info_sn = "Weirdo looking string, that I got from attributes section of vacuum entity in Xiaomi Miot Auto"

image

unGzipCommon(map_file, wifi_info_sn, owner_id, device_id, model, device_mac)

maksp86 avatar Mar 08 '24 06:03 maksp86

@PiotrMachowski Think my research would help ;-)

maksp86 avatar Mar 08 '24 07:03 maksp86

vacuum_map_parser_dreame raised Unsupported frame type error vacuum_map_parser_viomi parsed without errors but it seems like nothing is parsed Also it prints in console "223371 bytes remained in the buffer"

parsed_map output image

vacuum_map_parser_roborock raised IndexError: index out of range error somewhere in parsing code

I can send you a decrypted map file if you want

maksp86 avatar Mar 08 '24 07:03 maksp86

I m also very interested for this integration for the S12 Did you send the raw map? (It is listed in the first post as « unticked ») Thank you very much for your hard work! @PiotrMachowski @maksp86

xvolte avatar Mar 20 '24 07:03 xvolte

Could you please attach or publish somewhere then decrypted map file (before using any parser / parsing obviously) but already decrypted, to save time ? Thanks in advance

xvolte avatar Mar 23 '24 22:03 xvolte

Could you please attach or publish somewhere then decrypted map file (before using any parser / parsing obviously) but already decrypted, to save time ? Thanks in advance

Here you go decrypted_encrypted_map_test.tar.gz

maksp86 avatar Mar 24 '24 06:03 maksp86

Also there is all files needed to download map from miot-based vacuum (3C Enchanced, S10/S12, ijai.vacuum.*) map-download-kit-for-miot-vacuums.tar.gz I`ve also added patch files for two components

maksp86 avatar Mar 24 '24 07:03 maksp86

@

vacuum_map_parser_dreame raised Unsupported frame type error vacuum_map_parser_viomi parsed without errors but it seems like nothing is parsed Also it prints in console "223371 bytes remained in the buffer"

parsed_map output image

vacuum_map_parser_roborock raised IndexError: index out of range error somewhere in parsing code

I can send you a decrypted map file if you want

Maybe it is a dumb question but ... did you try the xiaomi map parser? what is the result ?

xvolte avatar Mar 24 '24 15:03 xvolte

Maybe it is a dumb question but ... did you try the xiaomi map parser? what is the result ?

It fails on parsing.. at least for me on line 55 of xiaomi_cloud_map_extractor\xiaomi\map_data_parser.py
block_type = MapDataParserXiaomi.get_int16(header, 0x00)

maksp86 avatar Mar 24 '24 16:03 maksp86

For information, the vacuum does not allow connection if the PC is on another subnet.... i connected in the same subnet, and now got a different error ...

now it fails here :

    if wifi_info_sn == None:
        raise Exception("Get wifi_info_sn failed")

it is strange as in your code, you seem to be working with a config option ? WIFI_INFO_SN

Traceback (most recent call last): File "C:\Users\XXXX\Downloads\xiaomimapextractor\master\map_downloader.py", line 59, in <module> main() File "C:\Users\XXXX\Downloads\xiaomimapextractor\master\map_downloader.py", line 35, in main raise Exception("Get wifi_info_sn failed") Exception: Get wifi_info_sn failed

xvolte avatar Mar 24 '24 16:03 xvolte

Are you on latest version of python-miio? You should use lib from their master git branch

maksp86 avatar Mar 24 '24 17:03 maksp86

yes i am. I have a xiaomi.vacuum.b106eu, maybe there are small adaptation to make, i don't know. but i don't have the wierd looking wifi string.... mine looks like this :

device.get_property_by(7, 45)[0]["value"]

'value': '[0,3,1,3,2,0,-3600,15,12.0,0,"fr_FR","xx23xxxxxxx46","en_US",0,-1,0,0,1,2,1]' --> i did replace the numbers with some xxx

The user_id is NOT present in this sentence, nor there is any semi column in the text. (i saw you are looking for semicolumn)

version of my robot is 4.3.3_0016

xvolte avatar Mar 24 '24 17:03 xvolte

Here you go decrypted_encrypted_map_test.tar.gz

Thanks! The decrypted version definitely looks like zlib-compressed data to me. ... managed to decompress it 0_decrypted_uncompressed.tar.gz

Tarh-76 avatar Mar 24 '24 19:03 Tarh-76

Ok I managed to get this from your file: map

The map starts at offset 0x75 in plain 8bit "grayscale" format. I couldn't figure out where the dimensions are stored. And the dimensions are definitely not fixed as I extracted my own map which is 800x800

Tarh-76 avatar Mar 30 '24 20:03 Tarh-76

Is there any news on this issue?

Borty97 avatar Apr 02 '24 16:04 Borty97

Is there any news on this issue?

I'm currently spending free time trying to decompile mi home plugin code for data extraction algorithm, but it much harder for understanding to me than code for decryption

maksp86 avatar Apr 02 '24 16:04 maksp86