Update custom components

Este commit está contenido en:
Florian Brinker
2021-08-28 21:21:19 +02:00
padre 4b4a1af316
commit 10c34b84f1
Se han modificado 1658 ficheros con 3572 adiciones y 701 borrados

Ver fichero

@@ -1,119 +1,62 @@
"""Support for Deebot Vaccums."""
import asyncio
import logging
import async_timeout
import time
import random
import string
import base64
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from datetime import timedelta
from deebotozmo import *
from homeassistant.util import Throttle
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['deebotozmo==1.7.8']
CONF_COUNTRY = "country"
CONF_CONTINENT = "continent"
CONF_DEVICEID = "deviceid"
CONF_LIVEMAPPATH = "livemappath"
CONF_LIVEMAP = "live_map"
CONF_SHOWCOLORROOMS = "show_color_rooms"
DEEBOT_DEVICES = "deebot_devices"
# Generate a random device ID on each bootup
DEEBOT_API_DEVICEID = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(8)
)
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import hub
from .const import DOMAIN, STARTUP
_LOGGER = logging.getLogger(__name__)
HUB = None
DOMAIN = 'deebot'
PLATFORMS = ["sensor", "binary_sensor", "vacuum", "camera"]
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string),
vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string),
vol.Required(CONF_DEVICEID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_LIVEMAP, default=True): cv.boolean,
vol.Optional(CONF_SHOWCOLORROOMS, default=False): cv.boolean,
vol.Optional(CONF_LIVEMAPPATH, default='www/'): cv.string
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Deebot."""
global HUB
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Deebot component."""
# Ensure our name space for storing objects is a known type. A dict is
# common/preferred as it allows a separate instance of your class for each
# instance that has been created in the UI.
hass.data.setdefault(DOMAIN, {})
HUB = DeebotHub(config[DOMAIN])
for component in ('sensor', 'binary_sensor', 'vacuum'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
# Print startup message
_LOGGER.info(STARTUP)
return True
class DeebotHub(Entity):
"""Deebot Hub"""
def __init__(self, domain_config):
"""Initialize the Deebot Vacuum."""
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Store an instance of the "connecting" class that does the work of speaking
# with your actual devices.
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
hub.DeebotHub, hass, entry.data
)
self.config = domain_config
self._lock = threading.Lock()
self.ecovacs_api = EcoVacsAPI(
DEEBOT_API_DEVICEID,
domain_config.get(CONF_USERNAME),
EcoVacsAPI.md5(domain_config.get(CONF_PASSWORD)),
domain_config.get(CONF_COUNTRY),
domain_config.get(CONF_CONTINENT)
)
# This creates each HA object for each platform your device requires.
# It's done by calling the `async_setup_entry` function in each platform module.
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
devices = self.ecovacs_api.devices()
liveMapEnabled = domain_config.get(CONF_LIVEMAP)
liveMapRooms = domain_config.get(CONF_SHOWCOLORROOMS)
country = domain_config.get(CONF_COUNTRY).lower()
continent = domain_config.get(CONF_CONTINENT).lower()
self.vacbots = []
return True
# CREATE VACBOT FOR EACH DEVICE
for device in devices:
if device['name'] in domain_config.get(CONF_DEVICEID):
vacbot = VacBot(
self.ecovacs_api.uid,
self.ecovacs_api.resource,
self.ecovacs_api.user_access_token,
device,
country,
continent,
liveMapEnabled,
liveMapRooms
)
_LOGGER.debug("New vacbot found: " + device['name'])
self.vacbots.append(vacbot)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
# This is called when an entry/configured device is to be removed. The class
# needs to unload itself, and remove callbacks. See the classes for further
# details
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
_LOGGER.debug("Hub initialized")
if unload_ok:
hass.data[DOMAIN][entry.entry_id].disconnect()
hass.data[DOMAIN].pop(entry.entry_id)
@Throttle(timedelta(seconds=10))
def update(self):
""" Update all statuses. """
try:
for vacbot in self.vacbots:
vacbot.request_all_statuses()
except Exception as ex:
_LOGGER.error('Update failed: %s', ex)
raise
@property
def name(self):
""" Return the name of the hub."""
return "Deebot Hub"
return unload_ok

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Archivo binario no mostrado.

Ver fichero

@@ -1,20 +1,25 @@
"""Support for Deebot Sensor."""
from typing import Optional
from typing import Optional, Dict, Any
from deebotozmo import *
from homeassistant.components.binary_sensor import BinarySensorEntity
from . import HUB as hub
from .const import DOMAIN
from .helpers import get_device_info
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deebot binary sensor platform."""
hub.update()
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add binary_sensor for passed config_entry in HA."""
hub = hass.data[DOMAIN][config_entry.entry_id]
new_devices = []
for vacbot in hub.vacbots:
add_devices([DeebotMopAttachedBinarySensor(vacbot, "mop_attached")], True)
new_devices.append(DeebotMopAttachedBinarySensor(vacbot, "mop_attached"))
if new_devices:
async_add_devices(new_devices)
class DeebotMopAttachedBinarySensor(BinarySensorEntity):
@@ -33,11 +38,20 @@ class DeebotMopAttachedBinarySensor(BinarySensorEntity):
self._name = self._vacbot_name + "_" + device_id
@property
def unique_id(self) -> str:
"""Return an unique ID."""
return self._vacbot.vacuum.get("did", None) + "_" + self._id
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def should_poll(self) -> bool:
return False
@property
def is_on(self):
return self._vacbot.mop_attached
@@ -46,3 +60,17 @@ class DeebotMopAttachedBinarySensor(BinarySensorEntity):
def icon(self) -> Optional[str]:
"""Return the icon to use in the frontend, if any."""
return "mdi:water" if self.is_on else "mdi:water-off"
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return True
@property
def device_info(self) -> Optional[Dict[str, Any]]:
return get_device_info(self._vacbot)
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
listener: EventListener = self._vacbot.waterEvents.subscribe(lambda _: self.schedule_update_ha_state())
self.async_on_remove(listener.unsubscribe)

Ver fichero

@@ -0,0 +1,65 @@
"""Support for Deebot Vaccums."""
import base64
import logging
from typing import Optional, Dict, Any
from homeassistant.components.camera import Camera
from .const import *
from .helpers import get_device_info
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add sensors for passed config_entry in HA."""
hub = hass.data[DOMAIN][config_entry.entry_id]
if hub.liveMapEnabled:
new_devices = []
for vacbot in hub.vacbots:
new_devices.append(DeeboLiveCamera(vacbot, "liveMap"))
if new_devices:
async_add_devices(new_devices)
class DeeboLiveCamera(Camera):
"""Deebot Live Camera"""
def __init__(self, vacbot, device_id):
"""Initialize the Deebot Vacuum."""
super().__init__()
self._vacbot = vacbot
self._id = device_id
if self._vacbot.vacuum.get("nick", None) is not None:
self._vacbot_name = "{}".format(self._vacbot.vacuum["nick"])
else:
# In case there is no nickname defined, use the device id
self._vacbot_name = "{}".format(self._vacbot.vacuum["did"])
self._name = self._vacbot_name + "_" + device_id
_LOGGER.debug("Camera initialized: %s", self.name)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def unique_id(self) -> str:
"""Return an unique ID."""
return self._vacbot.vacuum.get("did", None) + "_" + self._id
@property
def device_info(self) -> Optional[Dict[str, Any]]:
return get_device_info(self._vacbot)
async def async_camera_image(self):
"""Return a still image response from the camera."""
return base64.decodebytes(self._vacbot.live_map)

Ver fichero

@@ -0,0 +1,144 @@
"""Config flow for Deebot integration."""
import logging
import voluptuous as vol
import random
import string
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN
from .const import *
from deebotozmo import EcoVacsAPI, VacBot
_LOGGER = logging.getLogger(__name__)
# Generate a random device ID on each bootup
DEEBOT_API_DEVICEID = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(8)
)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_COUNTRY): str,
vol.Required(CONF_CONTINENT): str,
vol.Optional(CONF_LIVEMAP, default=False): bool,
vol.Optional(CONF_SHOWCOLORROOMS, default=False): bool,
}
)
async def validate_input(hass: core.HomeAssistant, data: dict):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
if len(data[CONF_COUNTRY]) != 2:
raise InvalidCountry
if len(data[CONF_CONTINENT]) != 2:
raise InvalidContinent
return await hass.async_add_executor_job(ConfigEntryRetriveRobots, hass, data)
def ConfigEntryRetriveRobots(hass: core.HomeAssistant, domain_config):
ecovacs_api = EcoVacsAPI(
DEEBOT_API_DEVICEID,
domain_config.get(CONF_USERNAME),
EcoVacsAPI.md5(domain_config.get(CONF_PASSWORD)),
domain_config.get(CONF_COUNTRY),
domain_config.get(CONF_CONTINENT),
)
return ecovacs_api.devices()
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Deebot."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
self.data = {}
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
self.robot_list = info
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidCountry:
errors[CONF_COUNTRY] = "invalid_country"
except InvalidContinent:
errors[CONF_CONTINENT] = "invalid_continent"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
self.data = user_input
robot_listDict = {e["name"]: e["nick"] for e in self.robot_list}
options_schema = vol.Schema(
{
vol.Required(
CONF_DEVICEID, default=list(robot_listDict.keys())
): cv.multi_select(robot_listDict)
}
)
return self.async_show_form(
step_id="robots", data_schema=options_schema, errors=errors
)
# If there is no user input or there were errors, show the form again, including any errors that were found with the input.
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_robots(self, user_input=None):
"""Handle the robots selection step."""
errors = {}
if user_input is not None:
try:
if len(user_input[CONF_DEVICEID]) < 1:
errors["base"] = "select_robots"
else:
self.data[CONF_DEVICEID] = user_input
return self.async_create_entry(
title=self.data[CONF_USERNAME], data=self.data
)
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
# If there is no user input or there were errors, show the form again, including any errors that were found with the input.
robot_listDict = {e["name"]: e["nick"] for e in self.robot_list}
options_schema = vol.Schema(
{
vol.Required(
CONF_DEVICEID, default=list(robot_listDict.keys())
): cv.multi_select(robot_listDict)
}
)
return self.async_show_form(
step_id="robots", data_schema=options_schema, errors=errors
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidCountry(exceptions.HomeAssistantError):
"""Error to indicate there is an invalid hostname."""
class InvalidContinent(exceptions.HomeAssistantError):
"""Error to indicate there is an invalid hostname."""

Ver fichero

@@ -0,0 +1,47 @@
from homeassistant.components.vacuum import (
PLATFORM_SCHEMA,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
SUPPORT_BATTERY,
SUPPORT_FAN_SPEED,
SUPPORT_LOCATE,
SUPPORT_PAUSE,
SUPPORT_RETURN_HOME,
SUPPORT_SEND_COMMAND,
SUPPORT_START,
SUPPORT_STATE,
VacuumEntity,
)
DOMAIN = "deebot"
INTEGRATION_VERSION = "2.1.2"
ISSUE_URL = "https://github.com/And3rsL/Deebot-for-Home-Assistant/issues"
STARTUP = f"""
-------------------------------------------------------------------
{DOMAIN}
Version: {INTEGRATION_VERSION}
This is a custom component
If you have any issues with this you need to open an issue here:
{ISSUE_URL}
-------------------------------------------------------------------
"""
CONF_COUNTRY = "country"
CONF_CONTINENT = "continent"
CONF_DEVICEID = "deviceid"
CONF_LIVEMAP = "live_map"
CONF_SHOWCOLORROOMS = "show_color_rooms"
DEEBOT_DEVICES = f"{DOMAIN}_devices"
STATE_CODE_TO_STATE = {
"STATE_IDLE": STATE_IDLE,
"STATE_CLEANING": STATE_CLEANING,
"STATE_RETURNING": STATE_RETURNING,
"STATE_DOCKED": STATE_DOCKED,
"STATE_ERROR": STATE_ERROR,
"STATE_PAUSED": STATE_PAUSED,
}

Ver fichero

@@ -0,0 +1,24 @@
from deebotozmo import VacBot
from .const import DOMAIN
def get_device_info(vacBot: VacBot):
device: dict = vacBot.vacuum
identifiers = set()
if "did" in device:
identifiers.add((DOMAIN, device.get("did")))
if "name" in device:
identifiers.add((DOMAIN, device.get("name")))
if not identifiers:
# we don't get a identifier to identify the device correctly abort
return None
return {
"identifiers": identifiers,
"name": device.get("nick", "Deebot vacuum"),
"manufacturer": "Ecovacs",
"model": device.get("deviceName", "Deebot vacuum"),
"sw_version": vacBot.fwversion,
}

Ver fichero

@@ -0,0 +1,76 @@
import logging
import random
import string
import threading
from deebotozmo import EcoVacsAPI, VacBot
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .const import *
_LOGGER = logging.getLogger(__name__)
# Generate a random device ID on each bootup
DEEBOT_API_DEVICEID = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(8)
)
class DeebotHub:
"""Deebot Hub"""
def __init__(self, hass: HomeAssistant, domain_config):
"""Initialize the Deebot Vacuum."""
self.config = domain_config
self._lock = threading.Lock()
self.hass = hass
self.ecovacs_api = EcoVacsAPI(
DEEBOT_API_DEVICEID,
domain_config.get(CONF_USERNAME),
EcoVacsAPI.md5(domain_config.get(CONF_PASSWORD)),
domain_config.get(CONF_COUNTRY),
domain_config.get(CONF_CONTINENT),
)
devices = self.ecovacs_api.devices()
liveMapEnabled = domain_config.get(CONF_LIVEMAP)
self.liveMapEnabled = liveMapEnabled
liveMapRooms = domain_config.get(CONF_SHOWCOLORROOMS)
country = domain_config.get(CONF_COUNTRY).lower()
continent = domain_config.get(CONF_CONTINENT).lower()
self.vacbots = []
# CREATE VACBOT FOR EACH DEVICE
for device in devices:
if device["name"] in domain_config.get(CONF_DEVICEID)[CONF_DEVICEID]:
vacbot = VacBot(
self.ecovacs_api.uid,
self.ecovacs_api.resource,
self.ecovacs_api.user_access_token,
device,
country,
continent,
liveMapEnabled,
liveMapRooms,
)
_LOGGER.debug("New vacbot found: " + device["name"])
vacbot.setScheduleUpdates()
self.vacbots.append(vacbot)
_LOGGER.debug("Hub initialized")
def disconnect(self):
for device in self.vacbots:
device.disconnect()
@property
def name(self):
""" Return the name of the hub."""
return "Deebot Hub"

Ver fichero

@@ -1,11 +1,14 @@
{
"domain": "deebot",
"name": "Deebot for Hassio",
"documentation": "https://github.com/And3rsL/Deebot-for-hassio",
"name": "Deebot for Home Assistant",
"version": "2.1.2",
"config_flow": true,
"documentation": "https://github.com/And3rsL/Deebot-for-Hassio",
"requirements": [
"deebotozmo==1.7.8"
"deebotozmo==1.8.0"
],
"dependencies": [],
"codeowners": ["@And3rsL"],
"homeassistant": "0.110.0"
}
"codeowners": [
"@And3rsL"
]
}

Ver fichero

@@ -1,51 +1,43 @@
"""Support for Deebot Sensor."""
from typing import Optional
import logging
from typing import Optional, Dict, Any
from deebotozmo import *
from homeassistant.const import (STATE_UNKNOWN)
from deebotozmo import (
COMPONENT_FILTER,
COMPONENT_SIDE_BRUSH,
COMPONENT_MAIN_BRUSH, EventListener,
)
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.entity import Entity
from . import HUB as hub
from .const import DOMAIN
from .helpers import get_device_info
_LOGGER = logging.getLogger(__name__)
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
)
STATE_CODE_TO_STATE = {
'STATE_IDLE': STATE_IDLE,
'STATE_CLEANING': STATE_CLEANING,
'STATE_RETURNING': STATE_RETURNING,
'STATE_DOCKED': STATE_DOCKED,
'STATE_ERROR': STATE_ERROR,
'STATE_PAUSED': STATE_PAUSED,
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deebot sensor."""
hub.update()
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add sensors for passed config_entry in HA."""
hub = hass.data[DOMAIN][config_entry.entry_id]
new_devices = []
for vacbot in hub.vacbots:
# General
add_devices([DeebotLastCleanImageSensor(vacbot, "last_clean_image")], True)
add_devices([DeebotWaterLevelSensor(vacbot, "water_level")], True)
new_devices.append(DeebotLastCleanImageSensor(vacbot, "last_clean_image"))
new_devices.append(DeebotWaterLevelSensor(vacbot, "water_level"))
# Components
add_devices([DeebotComponentSensor(vacbot, COMPONENT_MAIN_BRUSH)], True)
add_devices([DeebotComponentSensor(vacbot, COMPONENT_SIDE_BRUSH)], True)
add_devices([DeebotComponentSensor(vacbot, COMPONENT_FILTER)], True)
new_devices.append(DeebotComponentSensor(vacbot, COMPONENT_MAIN_BRUSH))
new_devices.append(DeebotComponentSensor(vacbot, COMPONENT_SIDE_BRUSH))
new_devices.append(DeebotComponentSensor(vacbot, COMPONENT_FILTER))
# Stats
add_devices([DeebotStatsSensor(vacbot, "stats_area")], True)
add_devices([DeebotStatsSensor(vacbot, "stats_time")], True)
add_devices([DeebotStatsSensor(vacbot, "stats_type")], True)
new_devices.append(DeebotStatsSensor(vacbot, "stats_area"))
new_devices.append(DeebotStatsSensor(vacbot, "stats_time"))
new_devices.append(DeebotStatsSensor(vacbot, "stats_type"))
if new_devices:
async_add_devices(new_devices)
class DeebotBaseSensor(Entity):
@@ -53,7 +45,6 @@ class DeebotBaseSensor(Entity):
def __init__(self, vacbot, device_id):
"""Initialize the Sensor."""
self._state = STATE_UNKNOWN
self._vacbot = vacbot
self._id = device_id
@@ -71,6 +62,24 @@ class DeebotBaseSensor(Entity):
"""Return the name of the device."""
return self._name
@property
def unique_id(self) -> str:
"""Return an unique ID."""
return self._vacbot.vacuum.get("did", None) + "_" + self._id
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return True
@property
def should_poll(self) -> bool:
return False
@property
def device_info(self) -> Optional[Dict[str, Any]]:
return get_device_info(self._vacbot)
class DeebotLastCleanImageSensor(DeebotBaseSensor):
"""Deebot Sensor"""
@@ -90,6 +99,11 @@ class DeebotLastCleanImageSensor(DeebotBaseSensor):
"""Return the icon to use in the frontend, if any."""
return "mdi:image-search"
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
listener: EventListener = self._vacbot.cleanLogsEvents.subscribe(lambda _: self.schedule_update_ha_state())
self.async_on_remove(listener.unsubscribe)
class DeebotWaterLevelSensor(DeebotBaseSensor):
"""Deebot Sensor"""
@@ -110,6 +124,11 @@ class DeebotWaterLevelSensor(DeebotBaseSensor):
"""Return the icon to use in the frontend, if any."""
return "mdi:water"
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
listener: EventListener = self._vacbot.waterEvents.subscribe(lambda _: self.schedule_update_ha_state())
self.async_on_remove(listener.unsubscribe)
class DeebotComponentSensor(DeebotBaseSensor):
"""Deebot Sensor"""
@@ -121,7 +140,7 @@ class DeebotComponentSensor(DeebotBaseSensor):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return '%'
return "%"
@property
def state(self):
@@ -139,6 +158,11 @@ class DeebotComponentSensor(DeebotBaseSensor):
elif self._id == COMPONENT_FILTER:
return "mdi:air-filter"
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
listener: EventListener = self._vacbot.lifespanEvents.subscribe(lambda _: self.schedule_update_ha_state())
self.async_on_remove(listener.unsubscribe)
class DeebotStatsSensor(DeebotBaseSensor):
"""Deebot Sensor"""
@@ -150,20 +174,20 @@ class DeebotStatsSensor(DeebotBaseSensor):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
if self._id == 'stats_area':
if self._id == "stats_area":
return "mq"
elif self._id == 'stats_time':
elif self._id == "stats_time":
return "min"
@property
def state(self):
"""Return the state of the vacuum cleaner."""
if self._id == 'stats_area' and self._vacbot.stats_area is not None:
if self._id == "stats_area" and self._vacbot.stats_area is not None:
return int(self._vacbot.stats_area)
elif self._id == 'stats_time' and self._vacbot.stats_time is not None:
return int(self._vacbot.stats_time/60)
elif self._id == 'stats_type':
elif self._id == "stats_time" and self._vacbot.stats_time is not None:
return int(self._vacbot.stats_time / 60)
elif self._id == "stats_type":
return self._vacbot.stats_type
else:
return STATE_UNKNOWN
@@ -171,9 +195,14 @@ class DeebotStatsSensor(DeebotBaseSensor):
@property
def icon(self) -> Optional[str]:
"""Return the icon to use in the frontend, if any."""
if self._id == 'stats_area':
if self._id == "stats_area":
return "mdi:floor-plan"
elif self._id == 'stats_time':
elif self._id == "stats_time":
return "mdi:timer-outline"
elif self._id == 'stats_type':
elif self._id == "stats_type":
return "mdi:cog"
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
listener: EventListener = self._vacbot.statsEvents.subscribe(lambda _: self.schedule_update_ha_state())
self.async_on_remove(listener.unsubscribe)

Ver fichero

@@ -0,0 +1,27 @@
{
"config": {
"abort": {
"already_configured": "Bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung",
"unknown": "Unerwarteter Fehler",
"invalid_country":"Ung\u00fcltiges Land! L\u00e4ndercode sollte aus 2 Zeichen bestehen! Bsp.: de, it, us, ...",
"invalid_continent":"Ung\u00fcltiger Kontinent! Code sollte aus 2 Zeichen bestehen! Bsp.: ww, eu, ...",
"select_robots": "Bitte w\u00e4hlen Sie mindestens 1 Roboter aus"
},
"step": {
"user": {
"data": {
"password": "Passwort",
"username": "E-mail oder ShortID",
"country": "Land",
"continent": "Kontinent",
"live_map": "Live-Karte aktiviert",
"show_color_rooms": "Raumfarbe anzeigen [BETA]"
}
}
}
}
}

Ver fichero

@@ -0,0 +1,27 @@
{
"config": {
"abort": {
"already_configured": "Alredy configured"
},
"error": {
"cannot_connect": "Can't connect to the ecovacs API",
"invalid_auth": "Invalid username or password",
"unknown": "Unknown error",
"invalid_country":"Country code should be two letter code, ex: us, uk, etc ",
"invalid_continent":"Continent code should be two letter code, ex: ww, eu, etc ",
"select_robots": "Please select at least 1 robot"
},
"step": {
"user": {
"data": {
"password": "Password",
"username": "E-mail or ShortID",
"country": "Country",
"continent": "Continent",
"live_map": "Live Map enabled",
"show_color_rooms": "Show room color [BETA]"
}
}
}
}
}

Ver fichero

@@ -0,0 +1,27 @@
{
"config": {
"abort": {
"already_configured": "Già configurato"
},
"error": {
"cannot_connect": "Non riesco a connettermi con ecovacs API",
"invalid_auth": "Username o password errate",
"unknown": "Errore sconosciuto",
"invalid_country":"Il country code deve essere di due lettere, es: it, us, uk etc ",
"invalid_continent":"Il Continent code deve essere di due lettere, es: eu, ww etc ",
"select_robots": "Seleziona almeno 1 robot"
},
"step": {
"user": {
"data": {
"password": "Password",
"username": "E-mail o ShortID",
"country": "Country",
"continent": "Continent",
"live_map": "Abilita Live Map",
"show_color_rooms": "Mostra stanze colorate [BETA]"
}
}
}
}
}

Ver fichero

@@ -1,79 +1,56 @@
"""Support for Deebot Vaccums."""
import base64
from typing import Optional, Dict, Any, Union, List
import logging
from typing import Optional, Dict, Any
from deebotozmo import *
from deebotozmo import (
FAN_SPEED_QUIET,
FAN_SPEED_NORMAL,
FAN_SPEED_MAX,
FAN_SPEED_MAXPLUS, VacBot, EventListener,
)
from homeassistant.core import HomeAssistant
from homeassistant.util import slugify
from . import HUB as hub
CONF_COUNTRY = "country"
CONF_CONTINENT = "continent"
CONF_DEVICEID = "deviceid"
CONF_LIVEMAPPATH = "livemappath"
CONF_LIVEMAP = "live_map"
CONF_SHOWCOLORROOMS = "show_color_rooms"
DEEBOT_DEVICES = "deebot_devices"
from homeassistant.components.vacuum import (
PLATFORM_SCHEMA,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
SUPPORT_BATTERY,
SUPPORT_FAN_SPEED,
SUPPORT_LOCATE,
SUPPORT_PAUSE,
SUPPORT_RETURN_HOME,
SUPPORT_SEND_COMMAND,
SUPPORT_START,
SUPPORT_STATE,
VacuumEntity,
)
from .const import *
from .helpers import get_device_info
_LOGGER = logging.getLogger(__name__)
SUPPORT_DEEBOT = (
SUPPORT_BATTERY
| SUPPORT_FAN_SPEED
| SUPPORT_LOCATE
| SUPPORT_PAUSE
| SUPPORT_RETURN_HOME
| SUPPORT_SEND_COMMAND
| SUPPORT_START
| SUPPORT_STATE
SUPPORT_BATTERY
| SUPPORT_FAN_SPEED
| SUPPORT_LOCATE
| SUPPORT_PAUSE
| SUPPORT_RETURN_HOME
| SUPPORT_SEND_COMMAND
| SUPPORT_START
| SUPPORT_STATE
)
STATE_CODE_TO_STATE = {
'STATE_IDLE': STATE_IDLE,
'STATE_CLEANING': STATE_CLEANING,
'STATE_RETURNING': STATE_RETURNING,
'STATE_DOCKED': STATE_DOCKED,
'STATE_ERROR': STATE_ERROR,
'STATE_PAUSED': STATE_PAUSED,
}
ATTR_COMPONENT_PREFIX = "component_"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deebot vacuums."""
if DEEBOT_DEVICES not in hass.data:
hass.data[DEEBOT_DEVICES] = []
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add sensors for passed config_entry in HA."""
hub = hass.data[DOMAIN][config_entry.entry_id]
new_devices = []
for vacbot in hub.vacbots:
vacuum = DeebotVacuum(hass, vacbot)
add_devices([vacuum])
new_devices.append(DeebotVacuum(hass, vacbot))
if new_devices:
async_add_devices(new_devices)
def _unsubscribe_listeners(listeners: [EventListener]):
for listener in listeners:
listener.unsubscribe()
class DeebotVacuum(VacuumEntity):
"""Deebot Vacuums"""
def __init__(self, hass, vacbot):
def __init__(self, hass: HomeAssistant, vacbot: VacBot):
"""Initialize the Deebot Vacuum."""
self._hass = hass
self.device = vacbot
if self.device.vacuum.get("nick", None) is not None:
@@ -84,11 +61,18 @@ class DeebotVacuum(VacuumEntity):
self._fan_speed = None
self._live_map = None
self._live_map_path = hub.config.get(CONF_LIVEMAPPATH) + self._name + '_liveMap.png'
self.device.refresh_statuses()
_LOGGER.debug("Vacuum initialized: %s", self.name)
self.att_data = {}
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
listeners = [
self.device.statusEvents.subscribe(lambda _: self.schedule_update_ha_state()),
self.device.batteryEvents.subscribe(lambda _: self.schedule_update_ha_state()),
self.device.roomEvents.subscribe(lambda _: self.schedule_update_ha_state()),
self.device.fanspeedEvents.subscribe(self.on_fan_change)
]
self.async_on_remove(lambda: _unsubscribe_listeners(listeners))
def on_fan_change(self, fan_speed):
self._fan_speed = fan_speed
@@ -96,7 +80,7 @@ class DeebotVacuum(VacuumEntity):
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state."""
return True
return False
@property
def unique_id(self) -> str:
@@ -165,58 +149,46 @@ class DeebotVacuum(VacuumEntity):
"""Send a command to a vacuum cleaner."""
_LOGGER.debug("async_send_command %s (%s), %s", command, params, kwargs)
if command == 'spot_area':
await self.hass.async_add_executor_job(self.device.SpotArea, params['rooms'], params['cleanings'])
if command == "spot_area":
await self.hass.async_add_executor_job(
self.device.SpotArea, params["rooms"], params["cleanings"]
)
return
if command == 'custom_area':
await self.hass.async_add_executor_job(self.device.CustomArea, params['coordinates'], params['cleanings'])
if command == "custom_area":
await self.hass.async_add_executor_job(
self.device.CustomArea, params["coordinates"], params["cleanings"]
)
return
if command == 'set_water':
await self.hass.async_add_executor_job(self.device.SetWaterLevel, params['amount'])
if command == "set_water":
await self.hass.async_add_executor_job(
self.device.SetWaterLevel, params["amount"]
)
return
if command == 'relocate':
if command == "relocate":
await self.hass.async_add_executor_job(self.device.Relocate)
return
if command == 'auto_clean':
self.hass.async_add_executor_job(self.device.Clean, params['type'])
if command == "auto_clean":
self.hass.async_add_executor_job(self.device.Clean, params["type"])
return
if command == 'refresh_components':
if command == "refresh_components":
await self.hass.async_add_executor_job(self.device.refresh_components)
return
if command == 'refresh_statuses':
if command == "refresh_statuses":
await self.hass.async_add_executor_job(self.device.refresh_statuses)
return
if command == 'refresh_live_map':
if command == "refresh_live_map":
await self.hass.async_add_executor_job(self.device.refresh_liveMap)
return
if command == 'save_live_map':
if(self._live_map != self.device.live_map):
self._live_map = self.device.live_map
with open(params['path'], "wb") as fh:
fh.write(base64.decodebytes(self.device.live_map))
await self.hass.async_add_executor_job(self.device.exc_command, command, params)
async def async_update(self):
"""Fetch state from the device."""
await self.hass.async_add_executor_job(self.device.request_all_statuses)
try:
if(self._live_map != self.device.live_map):
self._live_map = self.device.live_map
with open(self._live_map_path, "wb") as fh:
fh.write(base64.decodebytes(self.device.live_map))
except KeyError:
_LOGGER.warning("Can't access local folder: %s", self._live_map_path)
@property
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return device specific state attributes.
@@ -224,24 +196,28 @@ class DeebotVacuum(VacuumEntity):
Implemented by platform classes. Convention for attribute names
is lowercase snake_case.
"""
data: Dict[str, Union[int, List[int]]] = {}
# Needed for custom vacuum-card (https://github.com/denysdovhan/vacuum-card)
# Should find a better way without breaking everyone rooms script
data['status'] = STATE_CODE_TO_STATE[self.device.vacuum_status]
if self.device.getSavedRooms() is not None:
for r in self.device.getSavedRooms():
savedRooms = self.device.getSavedRooms()
if savedRooms is not None:
self.att_data = {}
for r in savedRooms:
# convert room name to snake_case to meet the convention
room_name = "room_" + slugify(r["subtype"])
room_values = data.get(room_name)
room_values = self.att_data.get(room_name)
if room_values is None:
data[room_name] = r["id"]
self.att_data[room_name] = r["id"]
elif isinstance(room_values, list):
room_values.append(r["id"])
else:
# Convert from int to list
data[room_name] = [room_values, r["id"]]
self.att_data[room_name] = [room_values, r["id"]]
return data
if self.device.vacuum_status:
self.att_data["status"] = STATE_CODE_TO_STATE[self.device.vacuum_status]
return self.att_data
@property
def device_info(self) -> Optional[Dict[str, Any]]:
return get_device_info(self.device)