diff --git a/__init__.py b/__init__.py index 7ecf1fd..eccfae9 100644 --- a/__init__.py +++ b/__init__.py @@ -8,6 +8,7 @@ from typing import Any from pybotvac.exceptions import NeatoException, NeatoRobotException from pybotvac.robot import Robot from pybotvac.vorwerk import Vorwerk +from pybotvac.session import PasswordlessSession import voluptuous as vol from homeassistant.components.vacuum import ( @@ -24,6 +25,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.const import CONF_TOKEN from .const import ( ACTION, @@ -97,6 +99,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool { VORWERK_ROBOT_API: r, VORWERK_ROBOT_COORDINATOR: _create_coordinator(hass, r), + CONF_TOKEN: entry.data[CONF_TOKEN] } for r in robot_states ] diff --git a/const.py b/const.py index e255fe8..093803c 100644 --- a/const.py +++ b/const.py @@ -23,6 +23,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" ATTR_ZONE = "zone" +ATTR_MAP = "map" ROBOT_STATE_INVALID = 0 ROBOT_STATE_IDLE = 1 diff --git a/manifest.json b/manifest.json index 6a7427d..ebe7529 100644 --- a/manifest.json +++ b/manifest.json @@ -2,16 +2,17 @@ "domain": "vorwerk", "name": "Vorwerk Kobold", "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/vorwerk", + "documentation": "https://github.com/trunneml/homeassistant-vorwerk", "requirements": [ - "pybotvac==0.0.20" + "pybotvac==0.0.23" ], "codeowners": [ - "@trunneml" + "@trunneml", + "@fuempel" ], "dependencies": [ "http" ], "iot_class": "cloud_polling", - "version": "0.9.6" + "version": "0.10.0" } \ No newline at end of file diff --git a/services.yaml b/services.yaml index 42d5660..97bb8d3 100644 --- a/services.yaml +++ b/services.yaml @@ -16,3 +16,6 @@ custom_cleaning: zone: description: Only supported on the VR300. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. example: "Kitchen" + map: + description: Only supported on the VR300. Name of the map for the zone to clean. Defaults to no zone i.e. complete house cleanup. + example: "Ground Floor" \ No newline at end of file diff --git a/translations/de.json b/translations/de.json index bad2a81..3044e96 100644 --- a/translations/de.json +++ b/translations/de.json @@ -6,14 +6,14 @@ "data": { "email": "E-Mailaddresse" }, - "description": "Um einen Authentifizierungscode per E-Mail zu erhalten, gib die E-Mailadresse deines Vorwerk-Accounts ein.\n\nSiehe [Vorwerk-Dokumentation]({docs_url})." + "description": "Um einen Authentifizierungscode per E-Mail zu erhalten, gib die E-Mailadresse deines Vorwerk-Accounts ein." }, "code": { "title": "Vorwerk Account Info", "data": { "code": "Code" }, - "description": "Gib den per E-Mail erhaltenen Code ein.\n\nSee [Vorwerk documentation]({docs_url})." + "description": "Gib den per E-Mail erhaltenen Code ein." } }, "error": { diff --git a/translations/en.json b/translations/en.json index 0cdcd3c..c82aafe 100644 --- a/translations/en.json +++ b/translations/en.json @@ -6,14 +6,14 @@ "data": { "email": "Email" }, - "description": "To recieve an authentication code, enter the email address of your vorwerk account.\n\nSee [Vorwerk documentation]({docs_url})." + "description": "To recieve an authentication code, enter the email address of your vorwerk account." }, "code": { "title": "Vorwerk Account Info", "data": { "code": "Code" }, - "description": "Enter the code you received by email.\n\nSee [Vorwerk documentation]({docs_url})." + "description": "Enter the code you received by email." } }, "error": { diff --git a/vacuum.py b/vacuum.py index 7009249..542749a 100644 --- a/vacuum.py +++ b/vacuum.py @@ -5,6 +5,8 @@ import logging from typing import Any from pybotvac import Robot +from pybotvac.session import PasswordlessSession +from pybotvac.account import Account from pybotvac.exceptions import NeatoRobotException import voluptuous as vol @@ -24,23 +26,24 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.const import ATTR_MODE +from homeassistant.const import ATTR_MODE, CONF_TOKEN from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) - from . import VorwerkState from .const import ( ATTR_CATEGORY, ATTR_NAVIGATION, ATTR_ZONE, + ATTR_MAP, VORWERK_DOMAIN, VORWERK_ROBOT_API, VORWERK_ROBOT_COORDINATOR, VORWERK_ROBOTS, + VORWERK_CLIENT_ID, ) _LOGGER = logging.getLogger(__name__) @@ -65,7 +68,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities( [ VorwerkConnectedVacuum( - robot[VORWERK_ROBOT_API], robot[VORWERK_ROBOT_COORDINATOR] + robot[VORWERK_ROBOT_API], robot[VORWERK_ROBOT_COORDINATOR], robot[CONF_TOKEN] ) for robot in hass.data[VORWERK_DOMAIN][entry.entry_id][VORWERK_ROBOTS] ], @@ -82,6 +85,7 @@ async def async_setup_entry(hass, entry, async_add_entities): vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int, vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int, vol.Optional(ATTR_ZONE): cv.string, + vol.Optional(ATTR_MAP): cv.string, }, "vorwerk_custom_cleaning", ) @@ -91,7 +95,7 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity): """Representation of a Vorwerk Connected Vacuum.""" def __init__( - self, robot_state: VorwerkState, coordinator: DataUpdateCoordinator[Any] + self, robot_state: VorwerkState, coordinator: DataUpdateCoordinator[Any], token ) -> None: """Initialize the Vorwerk Connected Vacuum.""" super().__init__(coordinator) @@ -100,7 +104,7 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity): self._name = f"{self.robot.name}" self._robot_serial = self.robot.serial - self._robot_boundaries: list = [] + self._token = token @property def name(self) -> str: @@ -214,23 +218,67 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity): ) def vorwerk_custom_cleaning( - self, mode: str, navigation: str, category: str, zone: str | None = None + self, mode: str, navigation: str, category: str, zone: str, map: str | None = None ) -> None: """Zone cleaning service call.""" + _LOGGER.debug("vorwerk_custom_cleaning called for %s / %s with token %s", map, zone, self._token) + + # create Vorwerk API session + account object and populate the robot list + # (this necessary to update pybotvac internal states) + session = PasswordlessSession(client_id=VORWERK_CLIENT_ID, token=self._token) + account = Account(session) + robots = account.robots + + _LOGGER.debug(" Robot list = %s", robots) + + map_id = None boundary_id = None - if zone is not None: - for boundary in self._robot_boundaries: - if zone in boundary["name"]: - boundary_id = boundary["id"] - if boundary_id is None: - _LOGGER.error( - "Zone '%s' was not found for the robot '%s'", zone, self.entity_id - ) + + if map is not None: + # search map + maps = account.persistent_maps[self._robot_serial] + + _LOGGER.debug(" Persistent map list = %s", maps) + + map_obj = None + available_maps = [] + for m in maps: + available_maps.append(m['name']) + if map in m['name']: + map_obj = m + + if map_obj is None: + _LOGGER.error("Map '%s' was not found for the robot '%s', list of valid maps: %s", map, self.entity_id, available_maps) return - _LOGGER.info("Start cleaning zone '%s' with robot %s", zone, self.entity_id) + + map_id = map_obj['id'] + _LOGGER.debug(" Found map %s = ID %s", map, map_id) + + if zone is not None: + # search zone = boundary ID + boundaries = self.robot.get_map_boundaries(map_id).json() + + _LOGGER.debug(" Boundary list = %s", boundaries) + + boundary_obj = None + available_zones = [] + for b in boundaries['data']['boundaries']: + available_zones.append(b['name']) + if zone in b['name']: + boundary_obj = b + + if boundary_obj is None: + _LOGGER.error("Zone '%s' was not found for the robot '%s' on map '%s', list of valid zones: %s", zone, self.entity_id, map, available_zones) + return + + boundary_id = boundary_obj['id'] + _LOGGER.debug(" Found baundary / zone %s = ID %s", zone, boundary_id) + + # start cleaning now + _LOGGER.info("Start cleaning zone '%s' on map '%s' with robot %s", zone, map, self.entity_id) try: - self.robot.start_cleaning(mode, navigation, category, boundary_id) + self.robot.start_cleaning(mode, navigation, category, boundary_id, map_id) except NeatoRobotException as ex: _LOGGER.error( "Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex