Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor
Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor copied to clipboard
Request for Xiaomi S20+ ( xiaomi.vacuum.b108gl )
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.b108gl
What is its name?
Xiaomi S20+ (S20 plus)
Available APIs
- [ ] xiaomi
- [ ] viomi
- [ ] roidmi
- [ ] dreame
Errors shown in the HA logs (if applicable)
2024-10-29 18:41:27.400 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 942, in async_update_ha_state
await self.async_device_update()
File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1302, in async_device_update
await hass.async_add_executor_job(self.update)
File "/usr/local/lib/python3.12/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
Please add this vacuum, if i can help - let me know please
Thank you!
Judging by the model number (xiaomi.vacuum.b108), I think it's the same integration as https://github.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor/issues/460 (xiaomi.vacuum.b106)
Adding my +1 Thank you
+1 waiting for this model
+1 here too - just got this popular model and would be awesome to have this working, thanks
Hi @PiotrMachowski , I have reverse engineered MI Home app plugin for S20+ and here is the python code that decrypts downloaded map. It should help with integration of S20+ to your map extractor.
from Crypto.Cipher import AES
from Crypto.Hash import MD5
from Crypto.Util.Padding import pad, unpad
import base64
import json
import zlib
import hashlib
def inflate(byte_array: bytes):
# Inflate using zlib
inflated_string = zlib.decompress(byte_array).decode('utf-8')
return inflated_string
def loadMapFromFile(file_path: str):
with open(file_path, 'rb') as f:
rawMapContent = f.read()
jsoMapContent = json.loads(rawMapContent)
return base64_decode(jsoMapContent["data"].encode('latin1'))
def encrypt(source: bytes, key: bytes, iv: bytes):
"""
Encrypts a string using AES encryption in CBC mode.
"""
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(source, AES.block_size))
return encrypted.hex().upper()
def decrypt(encrypted_bytes: bytes, key: bytes, iv: bytes):
"""
Decrypts a string using AES decryption in CBC mode.
"""
try:
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted_bytes)
decryptedUnpadded = unpad(decrypted, AES.block_size, 'pkcs7')
return decryptedUnpadded
except Exception as e:
return ""
def md5_hash(data: bytes):
"""
Returns the MD5 hash of the given data.
"""
return hashlib.md5(data).hexdigest()
def base64Encoding(input):
dataBase64 = base64.b64encode(input)
dataBase64P = dataBase64.decode("UTF-8")
return dataBase64P
def base64_decode(input: bytes):
"""
Decodes a Base64 string to hexadecimal.
"""
decoded_bytes = base64.decodebytes(input)
return decoded_bytes.hex()
def decryptMap(encryptedMapContent: bytes, modelKey: str, did: str):
originalWork = modelKey + did
iv = b"ABCDEF1234123412" # iv as a byte array
encKey = encrypt(originalWork.encode('latin1'), modelKey.encode('latin1'), iv)
encKey2 = bytes.fromhex(encKey)
md5Key = md5_hash(encKey2)
decryptKey = bytes.fromhex(md5Key)
encryptedBytes = bytes.fromhex(encryptedMapContent)
decryptedBase64Bytes = decrypt(encryptedBytes, decryptKey, iv)
inflatedString = inflate(decryptedBase64Bytes)
## Write decrypted map to file
#with open("0.decrypted.map.json", "w") as decryptedFile:
# # Writing data to a file
# decryptedFile.write(inflatedString)
return inflatedString
def transformMapData(map_data):
if map_data is None:
return None
map_data = json.loads(map_data)
map_id = map_data.get("map_id")
map_version = map_data.get("map_type")
map_height = map_data.get("height")
map_width = map_data.get("width")
origin_x = map_data.get("origin_x")
origin_y = map_data.get("origin_y")
have_charge_pile = map_data.get("have_pile")
charge_pile_x = map_data.get("pile_x")
charge_pile_y = map_data.get("pile_y")
charge_pile_yaw = map_data.get("pile_yaw")
map_resolution = map_data.get("resolution")
# Inflate the base64-encoded map data
byte_array = zlib.decompress(base64.b64decode(map_data.get("map_data")))
# Convert to a uint8 array (bytearray in Python)
data = bytearray(byte_array)
fb_walls = map_data.get("fb_walls")
fb_area = map_data.get("fb_regions")
zone = map_data.get("part_regions")
rooms = map_data.get("room_attrs")
room_colors = None
if isinstance(map_data.get("map_room_info"), list):
room_colors = {}
for item in map_data["map_room_info"]:
room_colors[item["grid_id"]] = item["color"]
path = map_data.get("paths")
# Charge details
charge = {
"haveChargePile": have_charge_pile,
"chargePileX": charge_pile_x,
"chargePileY": charge_pile_y,
"chargePileYaw": charge_pile_yaw,
}
# Origin details
origin = {
"x": round(origin_x / 1000, 2) if origin_x is not None else None,
"y": round(origin_y / 1000, 2) if origin_y is not None else None,
}
# Accuracy
accuracy = round(map_resolution / 1000, 2) if map_resolution is not None else None
# Header
header = {
"mapId": map_id,
"mapVersion": map_version,
"mapWidth": map_width,
"mapHeight": map_height,
"charge": charge,
"origin": origin,
"accuracy": accuracy,
"roomColors": room_colors,
}
# Extra
extra = {
"fbWalls": fb_walls,
"fbArea": fb_area,
"zone": zone,
"rooms": rooms,
"path": path,
}
# Final map structure
map_result = {
"header": header,
"data": data,
"extra": extra,
}
return map_result
def main():
modelKey = "mi.vacuum.b108gl"
did = "1068470163"
mapContent = loadMapFromFile("0.encrypted.map")
decryptedMapContent = decryptMap(mapContent, modelKey, did)
transformedMapData = transformMapData(decryptedMapContent)
print ('Map content:', transformedMapData)
if __name__ == "__main__":
main()
```
@mdudek that is great, thank you for your work! <3 I will check it out when I will have some free time
+1 and subscribed to updates
+1
+1
+1
+1
+1
+1
+1
Hi @PiotrMachowski , I have reverse engineered MI Home app plugin for S20+ and here is the python code that decrypts downloaded map. It should help with integration of S20+ to your map extractor.
from Crypto.Cipher import AES from Crypto.Hash import MD5 from Crypto.Util.Padding import pad, unpad import base64 import json import zlib import hashlib def inflate(byte_array: bytes): # Inflate using zlib inflated_string = zlib.decompress(byte_array).decode('utf-8') return inflated_string def loadMapFromFile(file_path: str): with open(file_path, 'rb') as f: rawMapContent = f.read() jsoMapContent = json.loads(rawMapContent) return base64_decode(jsoMapContent["data"].encode('latin1')) def encrypt(source: bytes, key: bytes, iv: bytes): """ Encrypts a string using AES encryption in CBC mode. """ cipher = AES.new(key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(pad(source, AES.block_size)) return encrypted.hex().upper() def decrypt(encrypted_bytes: bytes, key: bytes, iv: bytes): """ Decrypts a string using AES decryption in CBC mode. """ try: cipher = AES.new(key, AES.MODE_CBC, iv) decrypted = cipher.decrypt(encrypted_bytes) decryptedUnpadded = unpad(decrypted, AES.block_size, 'pkcs7') return decryptedUnpadded except Exception as e: return "" def md5_hash(data: bytes): """ Returns the MD5 hash of the given data. """ return hashlib.md5(data).hexdigest() def base64Encoding(input): dataBase64 = base64.b64encode(input) dataBase64P = dataBase64.decode("UTF-8") return dataBase64P def base64_decode(input: bytes): """ Decodes a Base64 string to hexadecimal. """ decoded_bytes = base64.decodebytes(input) return decoded_bytes.hex() def decryptMap(encryptedMapContent: bytes, modelKey: str, did: str): originalWork = modelKey + did iv = b"ABCDEF1234123412" # iv as a byte array encKey = encrypt(originalWork.encode('latin1'), modelKey.encode('latin1'), iv) encKey2 = bytes.fromhex(encKey) md5Key = md5_hash(encKey2) decryptKey = bytes.fromhex(md5Key) encryptedBytes = bytes.fromhex(encryptedMapContent) decryptedBase64Bytes = decrypt(encryptedBytes, decryptKey, iv) inflatedString = inflate(decryptedBase64Bytes) ## Write decrypted map to file #with open("0.decrypted.map.json", "w") as decryptedFile: # # Writing data to a file # decryptedFile.write(inflatedString) return inflatedString def transformMapData(map_data): if map_data is None: return None map_data = json.loads(map_data) map_id = map_data.get("map_id") map_version = map_data.get("map_type") map_height = map_data.get("height") map_width = map_data.get("width") origin_x = map_data.get("origin_x") origin_y = map_data.get("origin_y") have_charge_pile = map_data.get("have_pile") charge_pile_x = map_data.get("pile_x") charge_pile_y = map_data.get("pile_y") charge_pile_yaw = map_data.get("pile_yaw") map_resolution = map_data.get("resolution") # Inflate the base64-encoded map data byte_array = zlib.decompress(base64.b64decode(map_data.get("map_data"))) # Convert to a uint8 array (bytearray in Python) data = bytearray(byte_array) fb_walls = map_data.get("fb_walls") fb_area = map_data.get("fb_regions") zone = map_data.get("part_regions") rooms = map_data.get("room_attrs") room_colors = None if isinstance(map_data.get("map_room_info"), list): room_colors = {} for item in map_data["map_room_info"]: room_colors[item["grid_id"]] = item["color"] path = map_data.get("paths") # Charge details charge = { "haveChargePile": have_charge_pile, "chargePileX": charge_pile_x, "chargePileY": charge_pile_y, "chargePileYaw": charge_pile_yaw, } # Origin details origin = { "x": round(origin_x / 1000, 2) if origin_x is not None else None, "y": round(origin_y / 1000, 2) if origin_y is not None else None, } # Accuracy accuracy = round(map_resolution / 1000, 2) if map_resolution is not None else None # Header header = { "mapId": map_id, "mapVersion": map_version, "mapWidth": map_width, "mapHeight": map_height, "charge": charge, "origin": origin, "accuracy": accuracy, "roomColors": room_colors, } # Extra extra = { "fbWalls": fb_walls, "fbArea": fb_area, "zone": zone, "rooms": rooms, "path": path, } # Final map structure map_result = { "header": header, "data": data, "extra": extra, } return map_result def main(): modelKey = "mi.vacuum.b108gl" did = "1068470163" mapContent = loadMapFromFile("0.encrypted.map") decryptedMapContent = decryptMap(mapContent, modelKey, did) transformedMapData = transformMapData(decryptedMapContent) print ('Map content:', transformedMapData) if __name__ == "__main__": main() ```
Hi, where do I need to add this code?
Hi, where do I need to add this code?
It's not ready to be used
Hi, where do I need to add this code?
It's not ready to be used
Oh, well, good luck with the refinement, I'll look forward to when it's ready. Good luck!
Hi, where do I need to add this code?
It's not ready to be used
@PiotrMachowski - Might you be able to comment on how likely you are to be able to add support for the S20+ model at some point? It does seem to be a very popular request and it would be a big help to know whether support was, for example, imminent, months off or highly unlikely to ever happen. Generous support donation available! Thanks
Hi, where do I need to add this code?
It's not ready to be used
@PiotrMachowski - Might you be able to comment on how likely you are to be able to add support for the S20+ model at some point? It does seem to be a very popular request and it would be a big help to know whether support was, for example, imminent, months off or highly unlikely to ever happen. Generous support donation available! Thanks
+1, including the donation part.
@PiotrMachowski - Might you be able to comment on how likely you are to be able to add support for the S20+ model at some point? It does seem to be a very popular request and it would be a big help to know whether support was, for example, imminent, months off or highly unlikely to ever happen. Generous support donation available! Thanks
I did not have any free time recently, but now I slowly try to get back to this project. I have started rewriting this integration from the scratch, so it might take a while though. I also try to maintain my other repos (I have almost 50 of them...) and I also have to take care of my own HA instance - I have migrated to a new instance almost a year ago and I stil haven't created proper dashboards for it. In the meantime I also try to do some hardware projects (to not sit in front of a screen 24/7). Aaaand all of these things have to compete for my free time with other "normal" activities.
To sum up - I try my best to get back to this integration generally, not just specifically to this issue. I hope I will be able to release something usable in the next 2-3 months (but this is just a very rough guess).
By the way, "thumbs up" reaction to the original request is much better than commenting just "+1" under the issue. It sometimes gets irritating when you receive a lot of them from multiple threads/repos. When I release a new version of anything I evaluate all existing issues anyway.
Can someone make a json file with scripts for s20+ similar to this file? For mi home vevs version scenaries https://github.com/user-attachments/files/18538684/dreame.vacuum.p2009.json so that it would be possible to specify the room number and type of cleaning in the script
https://mi.vevs.me/mihome/files/old/MiHome_10.0.706_79976_vevs.apk
you can already achieve this, see: https://community.home-assistant.io/t/support-for-xiaomi-vacuum-s20-to-xiaomi-miio-integration/770596/20
Can someone make a json file with scripts for s20+ similar to this file? For mi home vevs version scenaries https://github.com/user-attachments/files/18538684/dreame.vacuum.p2009.json so that it would be possible to specify the room number and type of cleaning in the script
https://mi.vevs.me/mihome/files/old/MiHome_10.0.706_79976_vevs.apk
Hi, what does code avaiable tag means? Is there a commit I can test?
Hi, what does code avaiable tag means? Is there a commit I can test?
@rafaeltcc that's a note for me that there is a code posted in comments
Hello,
Any updates on here?
adding a +1 to this so I get alerts once added. Appreciate @PiotrMachowski is super busy and this is low on his life's priority list.
I left a tiny PayPal payment, if more people were to do this maybe it would encourage PiotrMachowski to find time to get back to coding on this project.
+1. Anybody knows if there is any other plugin to get this working?
Is there any plugin that supports S20+ except MIOT which doesn't support maps?
Is there any plugin that supports S20+ except MIOT which doesn't support maps?
I have this one and it works great.
Setup each room
whi
Is there any plugin that supports S20+ except MIOT which doesn't support maps?
I have this one and it works great. Setup each room
which plugin is that? does it support floor maps?