Skip to content

Commit

Permalink
Merge pull request #578 from PiotrMachowski/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
PiotrMachowski authored Jan 3, 2025
2 parents 2475e84 + 8837a96 commit 833b4f9
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 100 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
flake8-comprehensions isort mypy pytest pyupgrade safety
- run: bandit --recursive --skip B105,B108,B303,B304,B324,B311,B413,B506 .
- run: black --check . || true
- run: codespell --ignore-words-list="hass"
- run: codespell --ignore-words-list="hass" custom_components/
- run: flake8 custom_components --count --ignore=B001,E241,E265,E302,E722,E731,F403,F405,F841,W504
--max-complexity=21 --max-line-length=184 --show-source --statistics
- run: isort --check-only --profile black custom_components || true
Expand Down
32 changes: 21 additions & 11 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
---
name: Release

on:
release:
types: [published]

env:
COMPONENT_NAME: xiaomi_cloud_map_extractor

jobs:
release:
name: Prepare release
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Download repo
uses: actions/checkout@v1
uses: actions/checkout@v4.2.2

- name: Adjust version number
shell: bash
run: |
version="${{ github.event.release.tag_name }}"
yq e -P -o=json \
-i ".version = \"${version}\"" \
"${{ github.workspace }}/custom_components/${{ env.COMPONENT_NAME }}/manifest.json"
- name: Zip xiaomi_cloud_map_extractor dir
- name: Zip ${{ env.COMPONENT_NAME }} dir
run: |
cd /home/runner/work/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor/custom_components/xiaomi_cloud_map_extractor
zip xiaomi_cloud_map_extractor.zip -r ./
cd "${{ github.workspace }}/custom_components/${{ env.COMPONENT_NAME }}"
zip ${{ env.COMPONENT_NAME }}.zip -r ./
- name: Upload zip to release
uses: svenstaro/upload-release-action@v1-release
uses: softprops/action-gh-release@v2.1.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: /home/runner/work/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor/custom_components/xiaomi_cloud_map_extractor/xiaomi_cloud_map_extractor.zip
asset_name: xiaomi_cloud_map_extractor.zip
tag: ${{ github.ref }}
overwrite: true
files: ${{ github.workspace }}/custom_components/${{ env.COMPONENT_NAME }}/${{ env.COMPONENT_NAME }}.zip
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,16 @@ camera:
color_obstacle: [0, 0, 0, 127]
color_obstacle_with_photo: [0, 0, 0, 127]
color_path: [147, 194, 238]
color_mop_path: [255, 255, 255, 0x5F]
color_goto_path: [0, 255, 0]
color_predicted_path: [255, 255, 0, 0]
color_cleaned_area: [127, 127, 127, 127]
color_zones: [0xAD, 0xD8, 0xFF, 0x8F]
color_zones_outline: [0xAD, 0xD8, 0xFF]
color_virtual_walls: [255, 0, 0]
color_carpets: [0xA9, 0xF7, 0xA9 ]
color_no_carpet_zones: [255, 33, 55, 0x5F]
color_no_carpet_zones_outline: [255, 0, 0]
color_new_discovered_area: [64, 64, 64]
color_no_go_zones: [255, 33, 55, 127]
color_no_go_zones_outline: [255, 0, 0]
Expand Down Expand Up @@ -157,6 +161,8 @@ camera:
- goto_path
- ignored_obstacles
- ignored_obstacles_with_photo
- mop_path
- no_carpet_zones
- no_go_zones
- no_mopping_zones
- obstacles
Expand Down Expand Up @@ -190,12 +196,14 @@ camera:
charger_radius: 4
vacuum_radius: 6.5
path_width: 1
mop_path_width: 16
obstacle_radius: 3
ignored_obstacle_radius: 3
obstacle_with_photo_radius: 3
ignored_obstacle_with_photo_radius: 3
attributes:
- calibration_points
- carpet_map
- charger
- cleaned_rooms
- country
Expand All @@ -205,6 +213,8 @@ camera:
- image
- is_empty
- map_name
- mop_path
- no_carpet_areas
- no_go_areas
- no_mopping_areas
- obstacles
Expand Down Expand Up @@ -263,6 +273,7 @@ camera:

| Color name | Description |
| --- | --- |
| `color_carpets` | Carpets fill, in checkboard pattern |
| `color_charger` | Charger fill |
| `color_charger_outline` | Charger outline |
| `color_cleaned_area` | Fill of area that already has been cleaned |
Expand All @@ -275,13 +286,16 @@ camera:
| `color_map_wall_v2` | Walls (for software with rooms support) |
| `color_map_wall` | Walls (for software without rooms support) |
| `color_new_discovered_area` | Newly discovered areas |
| `color_no_carpet_zones_outline` | Outline of no-carpet zones |
| `color_no_carpet_zones` | Fill of no-carpet zones |
| `color_no_go_zones_outline` | Outline of no-go zones |
| `color_no_go_zones` | Fill of no-go zones |
| `color_no_mop_zones_outline` | Outline of no-mopping zones |
| `color_no_mop_zones` | Fill of no-mopping zones |
| `color_obstacle_with_photo` | Obstacle with photo mark on a map |
| `color_obstacle` | Obstacle mark on a map |
| `color_path` | Path of a vacuum |
| `color_mop_path` | Mopped path of a vacuum (for vacuums that support mopping) |
| `color_predicted_path` | Predicted path to a point in goto mode |
| `color_robo` | Vacuum fill |
| `color_robo_outline` | Vacuum outline |
Expand Down Expand Up @@ -310,6 +324,8 @@ camera:
- `goto_path`
- `ignored_obstacles_with_photo`
- `ignored_obstacles`
- `mop_path`
- `no_carpet_zones`
- `no_go_zones`
- `no_mopping_zones`
- `obstacles_with_photo`
Expand Down Expand Up @@ -357,13 +373,15 @@ fc-list | grep ttf | sed "s/.*\///"| sed "s/ttf.*/ttf/"
| `obstacle_with_photo_radius` | float | false | 3 | Radius of an obstacle with photo circle. |
| `ignored_obstacle_with_photo_radius` | float | false | 3 | Radius of an ignored obstacle with photo circle. |
| `path_width` | float | false | 1 | Width of path line. |
| `mop_path_width` | float | false | equal to vacuum radius | Width of mop path line. |

#### Attributes configuration

A list of attributes that an entity should have.
Available values:
- `calibration_points` - Calculated calibration points for [Lovelace Xiaomi Vacuum Map card](https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card).
<img src="https://raw.githubusercontent.com/PiotrMachowski/Home-Assistant-custom-components-Xiaomi-Cloud-Map-Extractor/master/images/map_card.gif" width=50%>
- `carpet_map`
- `charger`
- `cleaned_rooms`
- `country`
Expand All @@ -375,6 +393,8 @@ fc-list | grep ttf | sed "s/.*\///"| sed "s/ttf.*/ttf/"
- `image`
- `is_empty`
- `map_name`
- `mop_path`
- `no_carpet_areas`
- `no_go_areas`
- `no_mopping_areas`
- `obstacles_with_photo`
Expand Down Expand Up @@ -418,6 +438,7 @@ This integration was tested on following vacuums:
- `roborock.vacuum.a15` (Roborock S7)
- `roborock.vacuum.a19` (Roborocka S4 Max)
- `roborock.vacuum.a27` (Roborock S7 MaxV)
- `roborock.vacuum.a70` (Roborock S8 Pro Ultra)
- Viomi map format:
- `viomi.vacuum.v6` (Viomi Vacuum V2 Pro, Xiaomi Mijia STYJ02YM, Mi Robot Vacuum Mop Pro)
- `viomi.vacuum.v7` (Mi Robot Vacuum-Mop Pro)
Expand All @@ -426,7 +447,9 @@ This integration was tested on following vacuums:
- Roidmi map format:
- `roidmi.vacuum.v60` (Roidmi EVE Plus)
- `viomi.vacuum.v18` (Viomi S9)
- `viomi.vacuum.v38` (Viomi V5 Pro)
- `zhimi.vacuum.xa1` (Lydsto R1)
- `chuangmi.vacuum.hmi707` (IMILAB V1 Vacuum)
- Dreame map format:
- `dreame.vacuum.mc1808` (Xiaomi Mi Mop/Xiaomi Mijia 1C)
- `dreame.vacuum.p2008` (Dreame F9)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ action:
- condition: trigger
id: 0
- service: |
{% if trigger.to_state.state in ["unavailable", "unknown", "docked"] %}
{% if trigger.to_state.state in ["unavailable", "unknown", "docked", "idle", "paused", "charging"] %}
camera.turn_off
{% else %}
camera.turn_on
Expand Down
65 changes: 46 additions & 19 deletions custom_components/xiaomi_cloud_map_extractor/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from enum import Enum
from typing import Any, Dict, List, Optional

from custom_components.xiaomi_cloud_map_extractor.common.backoff import Backoff
from custom_components.xiaomi_cloud_map_extractor.common.map_data import MapData
from custom_components.xiaomi_cloud_map_extractor.common.vacuum import XiaomiCloudVacuum
from custom_components.xiaomi_cloud_map_extractor.types import Colors, Drawables, ImageConfig, Sizes, Texts
Expand All @@ -15,7 +16,7 @@
from miio import Vacuum as RoborockVacuum, DeviceException
import PIL.Image as Image
import voluptuous as vol
from homeassistant.components.camera import Camera, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SUPPORT_ON_OFF
from homeassistant.components.camera import Camera, CameraEntityFeature, ENTITY_ID_FORMAT, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import generate_entity_id
Expand Down Expand Up @@ -102,6 +103,8 @@
default=DEFAULT_SIZES[CONF_SIZE_VACUUM_RADIUS]): POSITIVE_FLOAT_SCHEMA,
vol.Optional(CONF_SIZE_PATH_WIDTH,
default=DEFAULT_SIZES[CONF_SIZE_PATH_WIDTH]): POSITIVE_FLOAT_SCHEMA,
vol.Optional(CONF_SIZE_MOP_PATH_WIDTH,
default=DEFAULT_SIZES[CONF_SIZE_VACUUM_RADIUS]): POSITIVE_FLOAT_SCHEMA,
vol.Optional(CONF_SIZE_IGNORED_OBSTACLE_RADIUS,
default=DEFAULT_SIZES[CONF_SIZE_IGNORED_OBSTACLE_RADIUS]): POSITIVE_FLOAT_SCHEMA,
vol.Optional(CONF_SIZE_IGNORED_OBSTACLE_WITH_PHOTO_RADIUS,
Expand Down Expand Up @@ -152,6 +155,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=


class VacuumCamera(Camera):
_map_name: Optional[str] = None

def __init__(self, entity_id: str, host: str, token: str, username: str, password: str, country: str, name: str,
should_poll: bool, image_config: ImageConfig, colors: Colors, drawables: Drawables, sizes: Sizes,
texts: Texts, attributes: List[str], store_map_raw: bool, store_map_image: bool, store_map_path: str,
Expand Down Expand Up @@ -181,7 +186,6 @@ def __init__(self, entity_id: str, host: str, token: str, username: str, passwor
self._map_data = None
self._logged_in = False
self._logged_in_previously = True
self._received_map_name_previously = True
self._country = country

async def async_added_to_hass(self) -> None:
Expand All @@ -205,8 +209,8 @@ def turn_off(self):
self._should_poll = False

@property
def supported_features(self) -> int:
return SUPPORT_ON_OFF
def supported_features(self):
return CameraEntityFeature.ON_OFF

@property
def extra_state_attributes(self) -> Dict[str, Any]:
Expand Down Expand Up @@ -236,6 +240,7 @@ def extract_attributes(map_data: MapData, attributes_to_return: List[str], count
rooms = list(map_data.rooms.keys())
for name, value in {
ATTRIBUTE_CALIBRATION: map_data.calibration(),
ATTRIBUTE_CARPET_MAP: map_data.carpet_map,
ATTRIBUTE_CHARGER: map_data.charger,
ATTRIBUTE_CLEANED_ROOMS: map_data.cleaned_rooms,
ATTRIBUTE_COUNTRY: country,
Expand All @@ -247,6 +252,8 @@ def extract_attributes(map_data: MapData, attributes_to_return: List[str], count
ATTRIBUTE_IMAGE: map_data.image,
ATTRIBUTE_IS_EMPTY: map_data.image.is_empty,
ATTRIBUTE_MAP_NAME: map_data.map_name,
ATTRIBUTE_MOP_PATH: map_data.mop_path,
ATTRIBUTE_NO_CARPET_AREAS: map_data.no_carpet_areas,
ATTRIBUTE_NO_GO_AREAS: map_data.no_go_areas,
ATTRIBUTE_NO_MOPPING_AREAS: map_data.no_mopping_areas,
ATTRIBUTE_OBSTACLES: map_data.obstacles,
Expand All @@ -270,15 +277,21 @@ def update(self):
self._handle_login()
if self._device is None and self._logged_in:
self._handle_device()
map_name = self._handle_map_name(counter)
if map_name == "retry" and self._device is not None:

new_map_name = self._handle_map_name(counter)
if new_map_name != "retry":
# sometimes this fails for no reason, so try and mitigate that by
# falling back to the previous map name if we have one
self._map_name = new_map_name

if self._map_name is None and self._device is not None:
self._status = CameraStatus.FAILED_TO_RETRIEVE_MAP_FROM_VACUUM
self._received_map_name_previously = map_name != "retry"
if self._logged_in and map_name != "retry" and self._device is not None:
self._handle_map_data(map_name)

if self._logged_in and self._map_name is not None and self._device is not None:
self._handle_map_data(self._map_name)
else:
_LOGGER.debug("Unable to retrieve map, reasons: Logged in - %s, map name - %s, device retrieved - %s",
self._logged_in, map_name, self._device is not None)
self._logged_in, new_map_name, self._device is not None)
self._set_map_data(MapDataParser.create_empty(self._colors, str(self._status)))
self._logged_in_previously = self._logged_in

Expand Down Expand Up @@ -310,23 +323,37 @@ def _handle_device(self):
self._status = CameraStatus.FAILED_TO_RETRIEVE_DEVICE

def _handle_map_name(self, counter: int) -> str:
"""
Downloads the map name from the vacuum. Sometimes the vacuum will just return
"retry" as the map for reasons unknown, so we'll try a few times before giving up.
We use exponential backoff to give the vacuum a chance to do whatever internal
processing it needs to do to get us a map name.
Pure speculation: perhaps the vacuum is busy trying to get a server connection
to be able to upload a map? When I run this command multiple times, there's an
incrementing number in the map names returned.
"""
map_name = "retry"
if self._device is not None and not self._device.should_get_map_from_vacuum():
map_name = "0"
while map_name == "retry" and counter > 0:
_LOGGER.debug("Retrieving map name from device")
time.sleep(0.1)

i = 1
backoff = Backoff(min_sleep=0.1, max_sleep=15)
while map_name == "retry" and i <= counter:
_LOGGER.debug("Asking device for map name... (%s/%s)", i, counter)
try:
map_name = self._vacuum.map()[0]
_LOGGER.debug("Map name %s", map_name)
if map_name != "retry":
_LOGGER.debug("Map name %s", map_name)
return map_name
except OSError as exc:
_LOGGER.error("Got OSError while fetching the state: %s", exc)
except DeviceException as exc:
if self._received_map_name_previously:
_LOGGER.warning("Got exception while fetching the state: %s", exc)
self._received_map_name_previously = False
finally:
counter = counter - 1
_LOGGER.warning("Got exception while fetching the state: %s", exc)

i += 1
time.sleep(backoff.backoff())
return map_name

def _handle_map_data(self, map_name: str):
Expand Down
14 changes: 14 additions & 0 deletions custom_components/xiaomi_cloud_map_extractor/common/backoff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import random


class Backoff:
"""Exponential backoff with jitter."""

def __init__(self, min_sleep, max_sleep):
self.min_sleep = min_sleep
self.sleep = min_sleep
self.max_sleep = max_sleep

def backoff(self):
self.sleep = min(self.max_sleep, random.uniform(self.min_sleep, self.sleep * 3))
return self.sleep
Loading

0 comments on commit 833b4f9

Please sign in to comment.