Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
314a9ef4a5 | |||
7274cd623f | |||
|
73589d9268 | ||
|
9c021a5226 | ||
|
024e5e5fae | ||
|
e19ab3e1bd | ||
|
418469cda7 | ||
|
2d1c6ecad8 | ||
|
ca06f15029 | ||
|
d00e44ca5d | ||
|
3a9297bc81 | ||
|
4f4a12d3fa | ||
|
ded13df09a |
22
__init__.py
22
__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 (
|
||||
@ -21,8 +22,10 @@ from homeassistant.components.vacuum import (
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
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,
|
||||
@ -96,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
|
||||
]
|
||||
@ -309,17 +313,15 @@ class VorwerkState:
|
||||
return self.robot_state["details"]["charge"]
|
||||
|
||||
@property
|
||||
def device_info(self) -> dict[str, str]:
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info for robot."""
|
||||
info = {
|
||||
"identifiers": {(VORWERK_DOMAIN, self.robot.serial)},
|
||||
"name": self.robot.name,
|
||||
}
|
||||
if self.robot_info:
|
||||
info["manufacturer"] = self.robot_info["battery"]["vendor"]
|
||||
info["model"] = self.robot_info["model"]
|
||||
info["sw_version"] = self.robot_info["firmware"]
|
||||
return info
|
||||
return DeviceInfo(
|
||||
identifiers={(VORWERK_DOMAIN, self.robot.serial)},
|
||||
manufacturer=self.robot_info["battery"]["vendor"] if self.robot_info else None,
|
||||
model=self.robot_info["model"] if self.robot_info else None,
|
||||
name=self.robot.name,
|
||||
sw_version=self.robot_info["firmware"] if self.robot_info else None,
|
||||
)
|
||||
|
||||
@property
|
||||
def schedule_enabled(self):
|
||||
|
1
const.py
1
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
|
||||
|
@ -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.2"
|
||||
"version": "0.10.0"
|
||||
}
|
5
repository.json
Normal file
5
repository.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Eriks homeassistant-vorwerk addon",
|
||||
"url": "https://git.sfs.ddnss.org/SFS/homeassistant-vorwerk",
|
||||
"maintainer": "Erik Foris <erikfor@outlook.de"
|
||||
}
|
@ -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"
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
118
vacuum.py
118
vacuum.py
@ -4,8 +4,10 @@ from __future__ import annotations
|
||||
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
|
||||
from pybotvac.robot import Robot
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
@ -24,22 +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__)
|
||||
@ -64,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]
|
||||
],
|
||||
@ -81,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",
|
||||
)
|
||||
@ -90,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)
|
||||
@ -99,47 +104,47 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
|
||||
self._name = f"{self.robot.name}"
|
||||
self._robot_serial = self.robot.serial
|
||||
self._robot_boundaries: list[str] = []
|
||||
self._token = token
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
def supported_features(self) -> int:
|
||||
"""Flag vacuum cleaner robot features that are supported."""
|
||||
return SUPPORT_VORWERK
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
def battery_level(self) -> int | None:
|
||||
"""Return the battery level of the vacuum cleaner."""
|
||||
return self._state.battery_level
|
||||
return int(self._state.battery_level) if self._state.battery_level else None
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
def available(self) -> bool:
|
||||
"""Return if the robot is available."""
|
||||
return self._state.available
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str:
|
||||
"""Return specific icon."""
|
||||
return "mdi:robot-vacuum-variant"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> str | None:
|
||||
"""Return the status of the vacuum cleaner."""
|
||||
return self._state.state if self._state else None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return self._robot_serial
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the vacuum cleaner."""
|
||||
data = {}
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
if self._state.status is not None:
|
||||
data[ATTR_STATUS] = self._state.status
|
||||
@ -147,11 +152,11 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
return data
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Device info for robot."""
|
||||
return self._state.device_info
|
||||
|
||||
def start(self):
|
||||
def start(self) -> None:
|
||||
"""Start cleaning or resume cleaning."""
|
||||
if not self._state:
|
||||
return
|
||||
@ -165,7 +170,7 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
)
|
||||
|
||||
def pause(self):
|
||||
def pause(self) -> None:
|
||||
"""Pause the vacuum."""
|
||||
try:
|
||||
self.robot.pause_cleaning()
|
||||
@ -174,7 +179,7 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
)
|
||||
|
||||
def return_to_base(self, **kwargs):
|
||||
def return_to_base(self, **kwargs: Any) -> None:
|
||||
"""Set the vacuum cleaner to return to the dock."""
|
||||
try:
|
||||
if self._state.state == STATE_CLEANING:
|
||||
@ -185,7 +190,7 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
)
|
||||
|
||||
def stop(self, **kwargs):
|
||||
def stop(self, **kwargs: Any) -> None:
|
||||
"""Stop the vacuum cleaner."""
|
||||
try:
|
||||
self.robot.stop_cleaning()
|
||||
@ -194,7 +199,7 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
)
|
||||
|
||||
def locate(self, **kwargs):
|
||||
def locate(self, **kwargs: Any) -> None:
|
||||
"""Locate the robot by making it emit a sound."""
|
||||
try:
|
||||
self.robot.locate()
|
||||
@ -203,7 +208,7 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
)
|
||||
|
||||
def clean_spot(self, **kwargs):
|
||||
def clean_spot(self, **kwargs: Any) -> None:
|
||||
"""Run a spot cleaning starting from the base."""
|
||||
try:
|
||||
self.robot.start_spot_cleaning()
|
||||
@ -212,21 +217,68 @@ class VorwerkConnectedVacuum(CoordinatorEntity, StateVacuumEntity):
|
||||
"Vorwerk vacuum connection error for '%s': %s", self.entity_id, ex
|
||||
)
|
||||
|
||||
def vorwerk_custom_cleaning(self, mode, navigation, category, zone=None):
|
||||
def vorwerk_custom_cleaning(
|
||||
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
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user