Compare commits

..

13 Commits
v0.9.3 ... main

Author SHA1 Message Date
314a9ef4a5 „repository.json“ hinzufügen 2022-11-25 19:40:20 +00:00
7274cd623f Merge pull request 'Enable map support' (#1) from yeet into main
Reviewed-on: #1
2022-11-22 10:26:43 +00:00
Fuempel
73589d9268
Fix translation issue during integration setup
Not working link causes strange dialog display during integration configuration phase.
2022-06-19 17:06:54 +02:00
Fuempel
9c021a5226
Review findings correction for zone cleaning support 2022-06-19 16:41:38 +02:00
Fuempel
024e5e5fae
Implement zone support for VR 300 2022-06-19 16:14:42 +02:00
Michael Graf
e19ab3e1bd Fix mypy error 2021-12-28 17:00:47 +01:00
Michael Graf
418469cda7 Bump Version 0.9.6 2021-12-28 16:44:15 +01:00
Michael Graf
2d1c6ecad8 Fix vacuum won't start when docked #4 2021-12-28 16:43:33 +01:00
Michael Graf
ca06f15029 Update manifest to 0.9.5 2021-12-28 12:42:18 +01:00
Michael Graf
d00e44ca5d Fix vacuum won't start when docked #4 2021-12-28 12:39:31 +01:00
Michael Graf
3a9297bc81
Merge pull request #13 from trunneml/dev
Release 0.9.4
2021-12-26 16:48:52 +01:00
Michael Graf
4f4a12d3fa Update version in manifest 2021-12-26 15:48:15 +00:00
Michael Graf
ded13df09a Use DeviceInfo and remove device_state_attributes #11 2021-12-26 15:48:15 +00:00
8 changed files with 115 additions and 51 deletions

View File

@ -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):

View File

@ -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

View File

@ -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
View 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"
}

View File

@ -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"

View File

@ -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": {

View File

@ -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
View File

@ -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