Skip to content
This repository was archived by the owner on Mar 8, 2022. It is now read-only.

Commit

Permalink
Merge pull request #91 from And3rsL/beta
Browse files Browse the repository at this point in the history
Config entry and various changes
  • Loading branch information
And3rsL authored Apr 26, 2021
2 parents a11a9ba + 09cf828 commit 0b2ac39
Show file tree
Hide file tree
Showing 19 changed files with 768 additions and 353 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Validate with hassfest

on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"

jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v2"
- uses: home-assistant/actions/hassfest@master
61 changes: 61 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# This GitHub action workflow is meant to be copyable to any repo that have the same structure.
# - Your integration exist under custom_components/{INTEGRATION_NAME}/[integration files]
# - You are using GitHub releases to publish new versions
# - You have a INTEGRATION_VERSION constant in custom_components/{INTEGRATION_NAME}/const.py

name: Release Workflow

on:
release:
types: [published]

jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@v2

- name: 🔢 Get release version
id: version
uses: home-assistant/actions/helpers/version@master

- name: ℹ️ Get integration information
id: information
run: |
name=$(find custom_components/ -type d -maxdepth 1 | tail -n 1 | cut -d "/" -f2)
echo "::set-output name=name::$name"
- name: 🖊️ Set version number
run: |
sed -i '/INTEGRATION_VERSION = /c\INTEGRATION_VERSION = "${{ steps.version.outputs.version }}"' \
"${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py"
jq '.version = "${{ steps.version.outputs.version }}"' \
"${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json" > tmp \
&& mv -f tmp "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json"
- name: 👀 Validate data
run: |
if ! grep -q 'INTEGRATION_VERSION = "${{ steps.version.outputs.version }}"' ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py; then
echo "The version in custom_components/${{ steps.information.outputs.name }}/const.py was not correct"
cat ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/const.py | grep INTEGRATION_VERSION
exit 1
fi
manifestversion=$(jq -r '.version' ${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/manifest.json)
if [ "$manifestversion" != "${{ steps.version.outputs.version }}" ]; then
echo "The version in custom_components/${{ steps.information.outputs.name }}/manifest.json was not correct"
echo "$manifestversion"
exit 1
fi
- name: 📦 Create zip file for the integration
run: |
cd "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}"
zip ${{ steps.information.outputs.name }}.zip -r ./
- name: 📤 Upload the zip file as a release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: "${{ github.workspace }}/custom_components/${{ steps.information.outputs.name }}/${{ steps.information.outputs.name }}.zip"
asset_name: ${{ steps.information.outputs.name }}.zip
asset_content_type: application/zip
47 changes: 4 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,7 @@ With this Home Assistant Custom Component you'll be able to
* and much more...

## Configuration
To add your Ecovacs devices into your Home Assistant installation, add the following to your configuration.yaml file:

```
# required fields
deebot:
username: YOUR_ECOVACS_USERNAME
password: YOUR_ECOVACS_PASSWORD
country: YOUR_TWO_LETTER_COUNTRY_CODE
continent: YOUR_TWO_LETTER_CONTINENT_CODE
deviceid:
- YOUR_ROBOT_ID
- YOUR_ROBOT_ID2
- etc...
# Optional
live_map: True # Enable Live Map.. may cause issues on low power hardware | Default: True
show_color_rooms: False # Enable draw room colors as in the app.. BE Carefull, very experimental. first thing to disable if there is any issue | Default: False
livemappath: 'www/' # Path where to save live_map (Each bot will have XXX_liveMap.png where XXX is the vacbot name)
```
To add your Ecovacs devices into your Home Assistant installation, follow the steps in Home Assistant -> Settings -> Integration -> Add -> Deebot for Home Assistant

### Chinese server Configuration
For chinese server username you require "short id" and password. short id look like "EXXXXXX". DO NOT USE YOUR MOBILE PHONE NUMBER, it wont work.
Expand All @@ -58,11 +41,6 @@ continent: as (or ww)

Since these servers are in china and unless you are close to china, don't expect very fast response.

### DeviceID
You can find your robot id under settings and "About Deebot" or inside the robot (normally under dust bin)

![Preview](images/deviceid.jpg)

### Country and Continent code
#### country:
Your two-letter country code (us, uk, etc).
Expand All @@ -83,6 +61,7 @@ Additional note: There are some issues during the password encoding. Using some

### Sensors
This integration expose a number of sensors
**All sensors are disabled by default.** You can enable only the required ones.

* sensor.ROBOTNAME_last_clean_image (A URL to the last success clean -- the image is on the ecovacs servers)
* sensor.ROBOTNAME_brush (% main brush)
Expand All @@ -95,26 +74,8 @@ This integration expose a number of sensors
* binary_sensor.ROBOTNAME_mop_attached (On/off is mop is attached)

### Live Map:
If is true live_map it will try to generate a live map in the specified folder
you can set a generic camera example:

Add Camera in configuration.yaml

```
camera:
- platform: generic
name: Deebot_live_map
still_image_url: "http://YOURLOCALIP:8123/local/YOUR_ROBOT_NAME_liveMap.png" #Example configuration for livemappath: 'www/'
verify_ssl: false
```

YAML interface:
```
type: picture-entity
entity: vacuum.YOUR_ROBOT_NAME
aspect_ratio: 50%
camera_image: camera.deebot_live_map
```
If is true live_map it will try to generate a live map camera feed
* camera.ROBOTNAME_liveMap

### Suggested yaml component
A suggested custom lovelace card that i use is: vacuum-card by denysdovhan link: https://github.com/denysdovhan/vacuum-card
Expand Down
149 changes: 46 additions & 103 deletions custom_components/deebot/__init__.py
Original file line number Diff line number Diff line change
@@ -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."""

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

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 = []
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
)

# 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'])
# 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)
)

self.vacbots.append(vacbot)

_LOGGER.debug("Hub initialized")
return True

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

if unload_ok:
hass.data[DOMAIN][entry.entry_id].disconnect()
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
40 changes: 34 additions & 6 deletions custom_components/deebot/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -33,11 +38,20 @@ def __init__(self, vacbot: VacBot, device_id: str):

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
Expand All @@ -46,3 +60,17 @@ def is_on(self):
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 False

@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())
Loading

0 comments on commit 0b2ac39

Please sign in to comment.