2021-03-20 16:11:04 +01:00
|
|
|
"""Support for Neato Connected Vacuums."""
|
2021-04-28 17:14:09 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-03-20 16:11:04 +01:00
|
|
|
import logging
|
2021-04-25 20:30:12 +02:00
|
|
|
from typing import Any
|
2021-03-20 16:11:04 +01:00
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
from pybotvac import Robot
|
2022-06-19 16:14:42 +02:00
|
|
|
from pybotvac.session import PasswordlessSession
|
|
|
|
from pybotvac.account import Account
|
2021-03-20 16:11:04 +01:00
|
|
|
from pybotvac.exceptions import NeatoRobotException
|
|
|
|
import voluptuous as vol
|
|
|
|
|
|
|
|
from homeassistant.components.vacuum import (
|
|
|
|
ATTR_STATUS,
|
|
|
|
STATE_CLEANING,
|
2021-12-28 12:35:55 +01:00
|
|
|
STATE_DOCKED,
|
2021-03-20 16:11:04 +01:00
|
|
|
STATE_IDLE,
|
|
|
|
STATE_PAUSED,
|
|
|
|
SUPPORT_BATTERY,
|
|
|
|
SUPPORT_CLEAN_SPOT,
|
|
|
|
SUPPORT_LOCATE,
|
|
|
|
SUPPORT_PAUSE,
|
|
|
|
SUPPORT_RETURN_HOME,
|
|
|
|
SUPPORT_START,
|
|
|
|
SUPPORT_STATE,
|
|
|
|
SUPPORT_STOP,
|
|
|
|
StateVacuumEntity,
|
|
|
|
)
|
2022-06-19 16:14:42 +02:00
|
|
|
from homeassistant.const import ATTR_MODE, CONF_TOKEN
|
2021-03-20 16:11:04 +01:00
|
|
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
2021-12-26 16:36:01 +01:00
|
|
|
from homeassistant.helpers.entity import DeviceInfo
|
2021-04-25 20:30:12 +02:00
|
|
|
from homeassistant.helpers.update_coordinator import (
|
|
|
|
CoordinatorEntity,
|
|
|
|
DataUpdateCoordinator,
|
|
|
|
)
|
2021-05-15 16:00:21 +02:00
|
|
|
from . import VorwerkState
|
2021-03-20 16:11:04 +01:00
|
|
|
from .const import (
|
2021-04-25 20:30:12 +02:00
|
|
|
ATTR_CATEGORY,
|
|
|
|
ATTR_NAVIGATION,
|
|
|
|
ATTR_ZONE,
|
2022-06-19 16:14:42 +02:00
|
|
|
ATTR_MAP,
|
2021-03-20 16:11:04 +01:00
|
|
|
VORWERK_DOMAIN,
|
2021-04-25 20:30:12 +02:00
|
|
|
VORWERK_ROBOT_API,
|
|
|
|
VORWERK_ROBOT_COORDINATOR,
|
2021-03-20 16:11:04 +01:00
|
|
|
VORWERK_ROBOTS,
|
2022-06-19 16:14:42 +02:00
|
|
|
VORWERK_CLIENT_ID,
|
2021-03-20 16:11:04 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
SUPPORT_VORWERK = (
|
|
|
|
SUPPORT_BATTERY
|
|
|
|
| SUPPORT_PAUSE
|
|
|
|
| SUPPORT_RETURN_HOME
|
|
|
|
| SUPPORT_STOP
|
|
|
|
| SUPPORT_START
|
|
|
|
| SUPPORT_CLEAN_SPOT
|
|
|
|
| SUPPORT_STATE
|
|
|
|
| SUPPORT_LOCATE
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(hass, entry, async_add_entities):
|
|
|
|
"""Set up Vorwerk vacuum with config entry."""
|
|
|
|
|
|
|
|
_LOGGER.debug("Adding vorwerk vacuums")
|
|
|
|
async_add_entities(
|
|
|
|
[
|
2021-04-25 20:30:12 +02:00
|
|
|
VorwerkConnectedVacuum(
|
2022-06-19 16:14:42 +02:00
|
|
|
robot[VORWERK_ROBOT_API], robot[VORWERK_ROBOT_COORDINATOR], robot[CONF_TOKEN]
|
2021-04-25 20:30:12 +02:00
|
|
|
)
|
2021-03-20 16:11:04 +01:00
|
|
|
for robot in hass.data[VORWERK_DOMAIN][entry.entry_id][VORWERK_ROBOTS]
|
|
|
|
],
|
|
|
|
True,
|
|
|
|
)
|
|
|
|
|
|
|
|
platform = entity_platform.current_platform.get()
|
|
|
|
assert platform is not None
|
|
|
|
|
|
|
|
platform.async_register_entity_service(
|
|
|
|
"custom_cleaning",
|
|
|
|
{
|
|
|
|
vol.Optional(ATTR_MODE, default=2): cv.positive_int,
|
|
|
|
vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int,
|
|
|
|
vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int,
|
|
|
|
vol.Optional(ATTR_ZONE): cv.string,
|
2022-06-19 16:14:42 +02:00
|
|
|
vol.Optional(ATTR_MAP): cv.string,
|
2021-03-20 16:11:04 +01:00
|
|
|
},
|
|
|
|
"vorwerk_custom_cleaning",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-04-25 20:30:12 +02:00
|
|
|
class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Representation of a Vorwerk Connected Vacuum."""
|
|
|
|
|
2021-04-25 20:30:12 +02:00
|
|
|
def __init__(
|
2022-06-19 16:14:42 +02:00
|
|
|
self, robot_state: VorwerkState, coordinator: DataUpdateCoordinator[Any], token
|
2021-04-25 20:30:12 +02:00
|
|
|
) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Initialize the Vorwerk Connected Vacuum."""
|
2021-04-25 20:30:12 +02:00
|
|
|
super().__init__(coordinator)
|
|
|
|
self.robot: Robot = robot_state.robot
|
|
|
|
self._state: VorwerkState = robot_state
|
|
|
|
|
2021-03-20 16:11:04 +01:00
|
|
|
self._name = f"{self.robot.name}"
|
|
|
|
self._robot_serial = self.robot.serial
|
2022-06-19 16:14:42 +02:00
|
|
|
self._robot_boundaries: list[str] = []
|
|
|
|
self._token = token
|
2021-03-20 16:11:04 +01:00
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def name(self) -> str:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Return the name of the device."""
|
|
|
|
return self._name
|
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def supported_features(self) -> int:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Flag vacuum cleaner robot features that are supported."""
|
|
|
|
return SUPPORT_VORWERK
|
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def battery_level(self) -> int | None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Return the battery level of the vacuum cleaner."""
|
2021-12-26 16:36:01 +01:00
|
|
|
return int(self._state.battery_level) if self._state.battery_level else None
|
2021-03-20 16:11:04 +01:00
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def available(self) -> bool:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Return if the robot is available."""
|
2021-04-25 20:30:12 +02:00
|
|
|
return self._state.available
|
2021-03-20 16:11:04 +01:00
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def icon(self) -> str:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Return specific icon."""
|
|
|
|
return "mdi:robot-vacuum-variant"
|
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def state(self) -> str | None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Return the status of the vacuum cleaner."""
|
2021-04-25 20:30:12 +02:00
|
|
|
return self._state.state if self._state else None
|
2021-03-20 16:11:04 +01:00
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def unique_id(self) -> str:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Return a unique ID."""
|
|
|
|
return self._robot_serial
|
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def extra_state_attributes(self) -> dict[str, Any]:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Return the state attributes of the vacuum cleaner."""
|
2021-12-26 16:36:01 +01:00
|
|
|
data: dict[str, Any] = {}
|
2021-03-20 16:11:04 +01:00
|
|
|
|
2021-04-25 20:30:12 +02:00
|
|
|
if self._state.status is not None:
|
|
|
|
data[ATTR_STATUS] = self._state.status
|
2021-03-20 16:11:04 +01:00
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
@property
|
2021-12-26 16:36:01 +01:00
|
|
|
def device_info(self) -> DeviceInfo:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Device info for robot."""
|
2021-04-25 20:30:12 +02:00
|
|
|
return self._state.device_info
|
2021-03-20 16:11:04 +01:00
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
def start(self) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Start cleaning or resume cleaning."""
|
2021-04-25 20:30:12 +02:00
|
|
|
if not self._state:
|
|
|
|
return
|
2021-03-20 16:11:04 +01:00
|
|
|
try:
|
2021-12-28 16:43:33 +01:00
|
|
|
if self._state.state == STATE_IDLE or self._state.state == STATE_DOCKED:
|
2021-03-20 16:11:04 +01:00
|
|
|
self.robot.start_cleaning()
|
2021-04-25 20:30:12 +02:00
|
|
|
elif self._state.state == STATE_PAUSED:
|
2021-03-20 16:11:04 +01:00
|
|
|
self.robot.resume_cleaning()
|
|
|
|
except NeatoRobotException as ex:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
)
|
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
def pause(self) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Pause the vacuum."""
|
|
|
|
try:
|
|
|
|
self.robot.pause_cleaning()
|
|
|
|
except NeatoRobotException as ex:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
)
|
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
def return_to_base(self, **kwargs: Any) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Set the vacuum cleaner to return to the dock."""
|
|
|
|
try:
|
2021-12-28 16:43:33 +01:00
|
|
|
if self._state.state == STATE_CLEANING:
|
2021-03-20 16:11:04 +01:00
|
|
|
self.robot.pause_cleaning()
|
|
|
|
self.robot.send_to_base()
|
|
|
|
except NeatoRobotException as ex:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
)
|
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
def stop(self, **kwargs: Any) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Stop the vacuum cleaner."""
|
|
|
|
try:
|
|
|
|
self.robot.stop_cleaning()
|
|
|
|
except NeatoRobotException as ex:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
)
|
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
def locate(self, **kwargs: Any) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Locate the robot by making it emit a sound."""
|
|
|
|
try:
|
|
|
|
self.robot.locate()
|
|
|
|
except NeatoRobotException as ex:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
)
|
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
def clean_spot(self, **kwargs: Any) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Run a spot cleaning starting from the base."""
|
|
|
|
try:
|
|
|
|
self.robot.start_spot_cleaning()
|
|
|
|
except NeatoRobotException as ex:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
)
|
|
|
|
|
2021-12-26 16:36:01 +01:00
|
|
|
def vorwerk_custom_cleaning(
|
2022-06-19 16:14:42 +02:00
|
|
|
self, mode: str, navigation: str, category: str, zone: str, map: str | None = None
|
2021-12-26 16:36:01 +01:00
|
|
|
) -> None:
|
2021-03-20 16:11:04 +01:00
|
|
|
"""Zone cleaning service call."""
|
2022-06-19 16:14:42 +02:00
|
|
|
_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
|
2021-03-20 16:11:04 +01:00
|
|
|
boundary_id = None
|
2022-06-19 16:14:42 +02:00
|
|
|
|
|
|
|
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)
|
2021-03-20 16:11:04 +01:00
|
|
|
return
|
2022-06-19 16:14:42 +02:00
|
|
|
|
|
|
|
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)
|
2021-03-20 16:11:04 +01:00
|
|
|
|
|
|
|
try:
|
2022-06-19 16:14:42 +02:00
|
|
|
self.robot.start_cleaning(mode, navigation, category, boundary_id, map_id)
|
2021-03-20 16:11:04 +01:00
|
|
|
except NeatoRobotException as ex:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
)
|
2022-06-19 16:14:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
# TODO: OLD CODE
|
|
|
|
# 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
|
|
|
|
# )
|
|
|
|
# return
|
|
|
|
# _LOGGER.info("Start cleaning zone '%s' with robot %s", zone, self.entity_id)
|
|
|
|
|
|
|
|
# try:
|
|
|
|
# self.robot.start_cleaning(mode, navigation, category, boundary_id)
|
|
|
|
# except NeatoRobotException as ex:
|
|
|
|
# _LOGGER.error(
|
|
|
|
# "Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
|
|
|
# )
|