diff --git a/.devcontainer/README.md b/.devcontainer/README.md
index bd429de..38dc4fd 100644
--- a/.devcontainer/README.md
+++ b/.devcontainer/README.md
@@ -8,8 +8,8 @@ In the container you will have a dedicated Home Assistant core instance running
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- Docker
- - For Linux, macOS, or Windows 10 Pro/Enterprise/Education use the [current release version of Docker](https://docs.docker.com/install/)
- - Windows 10 Home requires [WSL 2](https://docs.microsoft.com/windows/wsl/wsl2-install) and the current Edge version of Docker Desktop (see instructions [here](https://docs.docker.com/docker-for-windows/wsl-tech-preview/)). This can also be used for Windows Pro/Enterprise/Education.
+ - For Linux, macOS, or Windows 10 Pro/Enterprise/Education use the [current release version of Docker](https://docs.docker.com/install/)
+ - Windows 10 Home requires [WSL 2](https://docs.microsoft.com/windows/wsl/wsl2-install) and the current Edge version of Docker Desktop (see instructions [here](https://docs.docker.com/docker-for-windows/wsl-tech-preview/)). This can also be used for Windows Pro/Enterprise/Education.
- [Visual Studio code](https://code.visualstudio.com/)
- [Remote - Containers (VSC Extension)][extension-link]
@@ -35,12 +35,12 @@ When a task is currently running (like `Run Home Assistant on port 9123` for the
The available tasks are:
-Task | Description
--- | --
-Run Home Assistant on port 9123 | Launch Home Assistant with your custom component code and the configuration defined in `.devcontainer/configuration.yaml`.
-Run Home Assistant configuration against /config | Check the configuration.
-Upgrade Home Assistant to latest dev | Upgrade the Home Assistant core version in the container to the latest version of the `dev` branch.
-Install a specific version of Home Assistant | Install a specific version of Home Assistant core in the container.
+| Task | Description |
+| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
+| Run Home Assistant on port 9123 | Launch Home Assistant with your custom component code and the configuration defined in `.devcontainer/configuration.yaml`. |
+| Run Home Assistant configuration against /config | Check the configuration. |
+| Upgrade Home Assistant to latest dev | Upgrade the Home Assistant core version in the container to the latest version of the `dev` branch. |
+| Install a specific version of Home Assistant | Install a specific version of Home Assistant core in the container. |
### Step by Step debugging
@@ -54,7 +54,7 @@ by uncommenting the line:
# debugpy:
```
-Then launch the task `Run Home Assistant on port 9123`, and launch the debbuger
+Then launch the task `Run Home Assistant on port 9123`, and launch the debugger
with the existing debugging configuration `Python: Attach Local`.
For more information, look at [the Remote Python Debugger integration documentation](https://www.home-assistant.io/integrations/debugpy/).
diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml
index 4825311..659e4ed 100644
--- a/.devcontainer/configuration.yaml
+++ b/.devcontainer/configuration.yaml
@@ -1,11 +1,11 @@
-default_config:
-
-logger:
- default: info
- logs:
- custom_components.deebot: debug
- deebotozmo: debug
- components.vacuum.deebotozmo: debug
-
-# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
-debugpy:
+default_config:
+
+logger:
+ default: info
+ logs:
+ custom_components.deebot: debug
+ deebotozmo: debug
+ components.vacuum.deebotozmo: debug
+
+# If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
+debugpy:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index fab8c1f..676ff9e 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,30 +1,28 @@
-// See https://aka.ms/vscode-remote/devcontainer.json for format details.
-{
- "image": "ludeeus/container:integration-debian",
- "name": "Blueprint integration development",
- "context": "..",
- "appPort": [
- "9123:8123"
- ],
- "postCreateCommand": "container install",
- "extensions": [
- "ms-python.python",
- "github.vscode-pull-request-github",
- "ryanluker.vscode-coverage-gutters",
- "ms-python.vscode-pylance"
- ],
- "settings": {
- "files.eol": "\n",
- "editor.tabSize": 4,
- "terminal.integrated.shell.linux": "/bin/bash",
- "python.pythonPath": "/usr/bin/python3",
- "python.analysis.autoSearchPaths": false,
- "python.linting.pylintEnabled": true,
- "python.linting.enabled": true,
- "python.formatting.provider": "black",
- "editor.formatOnPaste": false,
- "editor.formatOnSave": true,
- "editor.formatOnType": true,
- "files.trimTrailingWhitespace": true
- }
-}
\ No newline at end of file
+// See https://aka.ms/vscode-remote/devcontainer.json for format details.
+{
+ "image": "ludeeus/container:integration-debian",
+ "name": "Blueprint integration development",
+ "context": "..",
+ "appPort": ["9123:8123"],
+ "postCreateCommand": "container install",
+ "extensions": [
+ "ms-python.python",
+ "github.vscode-pull-request-github",
+ "ryanluker.vscode-coverage-gutters",
+ "ms-python.vscode-pylance"
+ ],
+ "settings": {
+ "files.eol": "\n",
+ "editor.tabSize": 4,
+ "terminal.integrated.shell.linux": "/bin/bash",
+ "python.pythonPath": "/usr/bin/python3",
+ "python.analysis.autoSearchPaths": false,
+ "python.linting.pylintEnabled": true,
+ "python.linting.enabled": true,
+ "python.formatting.provider": "black",
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 3a7b4ec..6016401 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -13,8 +13,8 @@ body:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- - label: I have searched the existing issues and no issue is describing my issue
- required: true
+ - label: I have searched the existing issues and no issue is describing my issue
+ required: true
- type: textarea
validations:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index ec4bb38..3ba13e0 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1 +1 @@
-blank_issues_enabled: false
\ No newline at end of file
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 1db599e..ff33eb4 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -13,8 +13,8 @@ body:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for your feature request or idea
options:
- - label: I have searched the existing issues and no issue is describing my feature request or idea
- required: true
+ - label: I have searched the existing issues and no issue is describing my feature request or idea
+ required: true
- type: textarea
attributes:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..6de9891
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,17 @@
+version: 2
+updates:
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ labels:
+ - "pr: dependency-update"
+ schedule:
+ interval: "daily"
+
+ # Maintain dependencies for pip
+ - package-ecosystem: "pip"
+ directory: "/"
+ labels:
+ - "pr: dependency-update"
+ schedule:
+ interval: "daily"
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 0000000..a2a8b49
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,51 @@
+name-template: "$RESOLVED_VERSION"
+tag-template: "$RESOLVED_VERSION"
+change-template: "- #$NUMBER $TITLE @$AUTHOR"
+sort-direction: ascending
+prerelease: true
+categories:
+ - title: ":boom: Breaking changes"
+ label: "Breaking Change"
+
+ - title: ":sparkles: New features"
+ label: "pr: new-feature"
+
+ - title: ":zap: Enhancements"
+ label: "pr: enhancement"
+
+ - title: ":recycle: Refactor"
+ label: "pr: refactor"
+
+ - title: ":bug: Bug Fixes"
+ label: "pr: bugfix"
+
+ - title: ":arrow_up: Dependency Updates"
+ labels:
+ - "pr: dependency-update"
+ - "dependencies"
+
+include-labels:
+ - "Breaking Change"
+ - "pr: enhancement"
+ - "pr: dependency-update"
+ - "pr: new-feature"
+ - "pr: bugfix"
+ - "pr: refactor"
+
+version-resolver:
+ major:
+ labels:
+ - "Breaking Change"
+ minor:
+ labels:
+ - "pr: enhancement"
+ - "pr: dependency-update"
+ - "pr: new-feature"
+ patch:
+ labels:
+ - "pr: bugfix"
+ default: minor
+
+template: |
+ [![Downloads for this release](https://img.shields.io/github/downloads/And3rsL/Deebotozmo/$RESOLVED_VERSION/total.svg)](https://github.com/And3rsL/Deebotozmo/releases/$RESOLVED_VERSION)
+ $CHANGES
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6458d85
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,49 @@
+# This workflow will install Python dependencies, run tests and lint with a single version of Python
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ pull_request: ~
+
+env:
+ DEFAULT_PYTHON: 3.9
+
+jobs:
+ validate:
+ runs-on: "ubuntu-latest"
+ name: Validate
+ steps:
+ - uses: "actions/checkout@v2"
+
+ - name: HACS validation
+ uses: "hacs/action@main"
+ with:
+ category: "integration"
+ ignore: brands
+
+ - name: Hassfest validation
+ uses: "home-assistant/actions/hassfest@master"
+
+ code-quality:
+ runs-on: "ubuntu-latest"
+ name: Check code quality
+ steps:
+ - uses: "actions/checkout@v2"
+ - name: Set up Python ${{ env.DEFAULT_PYTHON }}
+ id: python
+ uses: actions/setup-python@v2.2.2
+ with:
+ python-version: ${{ env.DEFAULT_PYTHON }}
+ - name: Install dependencies
+ run: |
+ pip install -r requirements.txt
+ - name: Run pre-commit checks
+ run: |
+ pre-commit run --hook-stage manual --all-files --show-diff-on-failure
+ - name: Pylint review
+ run: |
+ pylint custom_components
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..4b538c2
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,73 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches:
+ - master
+ schedule:
+ - cron: "20 10 * * 0"
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ["python"]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+ # Learn more:
+ # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v1
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ # - run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/cron.yaml b/.github/workflows/cron.yaml
index 8d51d0f..8eeb9be 100644
--- a/.github/workflows/cron.yaml
+++ b/.github/workflows/cron.yaml
@@ -2,20 +2,20 @@ name: Cron actions
on:
schedule:
- - cron: '0 0 * * *'
+ - cron: "0 0 * * *"
jobs:
validate:
runs-on: "ubuntu-latest"
name: Validate
steps:
- - uses: "actions/checkout@v2"
+ - uses: "actions/checkout@v2"
- - name: HACS validation
- uses: "hacs/action@main"
- with:
- category: "integration"
- ignore: brands
+ - name: HACS validation
+ uses: "hacs/action@main"
+ with:
+ category: "integration"
+ ignore: brands
- - name: Hassfest validation
- uses: "home-assistant/actions/hassfest@master"
\ No newline at end of file
+ - name: Hassfest validation
+ uses: "home-assistant/actions/hassfest@master"
diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml
deleted file mode 100644
index 4903b77..0000000
--- a/.github/workflows/pull.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-name: Pull actions
-
-on:
- pull_request:
-
-jobs:
- validate:
- runs-on: "ubuntu-latest"
- name: Validate
- steps:
- - uses: "actions/checkout@v2"
-
- - name: HACS validation
- uses: "hacs/action@main"
- with:
- category: "integration"
- ignore: brands
-
- - name: Hassfest validation
- uses: "home-assistant/actions/hassfest@master"
-
- style:
- runs-on: "ubuntu-latest"
- name: Check style formatting
- steps:
- - uses: "actions/checkout@v2"
- - uses: "actions/setup-python@v1"
- with:
- python-version: "3.x"
- - run: python3 -m pip install black
- - run: black .
-
- # tests:
- # runs-on: "ubuntu-latest"
- # name: Run tests
- # steps:
- # - name: Check out code from GitHub
- # uses: "actions/checkout@v2"
- # - name: Setup Python
- # uses: "actions/setup-python@v1"
- # with:
- # python-version: "3.8"
- # - name: Install requirements
- # run: python3 -m pip install -r requirements_test.txt
- # - name: Run tests
- # run: |
- # pytest \
- # -qq \
- # --timeout=9 \
- # --durations=10 \
- # -n auto \
- # --cov custom_components.deebot \
- # -o console_output_style=count \
- # -p no:sugar \
- # tests
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
deleted file mode 100644
index 28e17e3..0000000
--- a/.github/workflows/push.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: Push actions
-
-on:
- push:
- branches:
- - master
- - dev
-
-jobs:
- validate:
- runs-on: "ubuntu-latest"
- name: Validate
- steps:
- - uses: "actions/checkout@v2"
-
- - name: HACS validation
- uses: "hacs/action@main"
- with:
- category: "integration"
- ignore: brands
-
- - name: Hassfest validation
- uses: "home-assistant/actions/hassfest@master"
-
- style:
- runs-on: "ubuntu-latest"
- name: Check style formatting
- steps:
- - uses: "actions/checkout@v2"
- - uses: "actions/setup-python@v1"
- with:
- python-version: "3.x"
- - run: python3 -m pip install black
- - run: black .
-
- # tests:
- # runs-on: "ubuntu-latest"
- # name: Run tests
- # steps:
- # - name: Check out code from GitHub
- # uses: "actions/checkout@v2"
- # - name: Setup Python
- # uses: "actions/setup-python@v1"
- # with:
- # python-version: "3.8"
- # - name: Install requirements
- # run: python3 -m pip install -r requirements_test.txt
- # - name: Run tests
- # run: |
- # pytest \
- # -qq \
- # --timeout=9 \
- # --durations=10 \
- # -n auto \
- # --cov custom_components.deebot \
- # -o console_output_style=count \
- # -p no:sugar \
- # tests
\ No newline at end of file
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
new file mode 100644
index 0000000..17fdb96
--- /dev/null
+++ b/.github/workflows/release-drafter.yml
@@ -0,0 +1,14 @@
+name: Release Drafter
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ update_release_draft:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: release-drafter/release-drafter@v5
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 9bcc0c0..22b56ea 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -69,4 +69,4 @@ jobs:
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
\ No newline at end of file
+ asset_content_type: application/zip
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..9aeae9f
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,74 @@
+repos:
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.28.1
+ hooks:
+ - id: pyupgrade
+ args: [--py37-plus]
+ - repo: https://github.com/psf/black
+ rev: 21.9b0
+ hooks:
+ - id: black
+ args:
+ - --safe
+ - --quiet
+ files: ^(custom_components/.+)?[^/]+\.py$
+ - repo: https://github.com/codespell-project/codespell
+ rev: v2.1.0
+ hooks:
+ - id: codespell
+ args:
+ - --ignore-words-list=hass,deebot
+ - --skip="./.*,*.csv,*.json"
+ - --quiet-level=2
+ exclude_types: [csv, json]
+ - repo: https://gitlab.com/pycqa/flake8
+ rev: 3.9.2
+ hooks:
+ - id: flake8
+ additional_dependencies:
+ - flake8-docstrings==1.6.0
+ - pydocstyle==6.1.1
+ files: ^(custom_components/.+)?[^/]+\.py$
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.7.0
+ hooks:
+ - id: bandit
+ args:
+ - --quiet
+ - --format=custom
+ - --configfile=bandit.yaml
+ files: ^(custom_components/.+)?[^/]+\.py$
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.9.3
+ hooks:
+ - id: isort
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.0.1
+ hooks:
+ - id: check-executables-have-shebangs
+ stages: [manual]
+ - id: no-commit-to-branch
+ args:
+ - --branch=master
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v2.4.1
+ hooks:
+ - id: prettier
+ stages: [manual]
+ - repo: https://github.com/adrienverge/yamllint.git
+ rev: v1.26.3
+ hooks:
+ - id: yamllint
+ - repo: local
+ hooks:
+ # Run mypy through our wrapper script in order to get the possible
+ # pyenv and/or virtualenv activated; it may not have been e.g. if
+ # committing from a GUI tool that was not launched from an activated
+ # shell.
+ - id: mypy
+ name: mypy
+ entry: scripts/run-in-env.sh mypy
+ language: script
+ types: [python]
+ require_serial: true
+ files: ^(custom_components/.+)?[^/]+\.py$
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 555a62b..cc5337a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,35 +1,34 @@
{
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
- "version": "0.2.0",
- "configurations": [
- {
- // Example of attaching to local debug server
- "name": "Python: Attach Local",
- "type": "python",
- "request": "attach",
- "port": 5678,
- "host": "localhost",
- "pathMappings": [
- {
- "localRoot": "${workspaceFolder}",
- "remoteRoot": "."
- }
- ]
- },
- {
- // Example of attaching to my production server
- "name": "Python: Attach Remote",
- "type": "python",
- "request": "attach",
- "port": 5678,
- "host": "homeassistant.local",
- "pathMappings": [
- {
- "localRoot": "${workspaceFolder}",
- "remoteRoot": "/usr/src/homeassistant"
- }
- ]
- }
- ]
- }
-
\ No newline at end of file
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ // Example of attaching to local debug server
+ "name": "Python: Attach Local",
+ "type": "python",
+ "request": "attach",
+ "port": 5678,
+ "host": "localhost",
+ "pathMappings": [
+ {
+ "localRoot": "${workspaceFolder}",
+ "remoteRoot": "."
+ }
+ ]
+ },
+ {
+ // Example of attaching to my production server
+ "name": "Python: Attach Remote",
+ "type": "python",
+ "request": "attach",
+ "port": 5678,
+ "host": "homeassistant.local",
+ "pathMappings": [
+ {
+ "localRoot": "${workspaceFolder}",
+ "remoteRoot": "/usr/src/homeassistant"
+ }
+ ]
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a3d535d..35cca1e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,8 +1,8 @@
{
- "python.linting.pylintEnabled": true,
- "python.linting.enabled": true,
- "python.pythonPath": "/usr/local/bin/python",
- "files.associations": {
- "*.yaml": "home-assistant"
- }
-}
\ No newline at end of file
+ "python.linting.pylintEnabled": true,
+ "python.linting.enabled": true,
+ "python.pythonPath": "/usr/local/bin/python",
+ "files.associations": {
+ "*.yaml": "home-assistant"
+ }
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index d1a0ae7..47f1210 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,29 +1,29 @@
-{
- "version": "2.0.0",
- "tasks": [
- {
- "label": "Run Home Assistant on port 9123",
- "type": "shell",
- "command": "container start",
- "problemMatcher": []
- },
- {
- "label": "Run Home Assistant configuration against /config",
- "type": "shell",
- "command": "container check",
- "problemMatcher": []
- },
- {
- "label": "Upgrade Home Assistant to latest dev",
- "type": "shell",
- "command": "container install",
- "problemMatcher": []
- },
- {
- "label": "Install a specific version of Home Assistant",
- "type": "shell",
- "command": "container set-version",
- "problemMatcher": []
- }
- ]
-}
\ No newline at end of file
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Run Home Assistant on port 9123",
+ "type": "shell",
+ "command": "container start",
+ "problemMatcher": []
+ },
+ {
+ "label": "Run Home Assistant configuration against /config",
+ "type": "shell",
+ "command": "container check",
+ "problemMatcher": []
+ },
+ {
+ "label": "Upgrade Home Assistant to latest dev",
+ "type": "shell",
+ "command": "container install",
+ "problemMatcher": []
+ },
+ {
+ "label": "Install a specific version of Home Assistant",
+ "type": "shell",
+ "command": "container set-version",
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/.yamllint b/.yamllint
new file mode 100644
index 0000000..96b89d6
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,59 @@
+rules:
+ braces:
+ level: error
+ min-spaces-inside: 0
+ max-spaces-inside: 1
+ min-spaces-inside-empty: -1
+ max-spaces-inside-empty: -1
+ brackets:
+ level: error
+ min-spaces-inside: 0
+ max-spaces-inside: 0
+ min-spaces-inside-empty: -1
+ max-spaces-inside-empty: -1
+ colons:
+ level: error
+ max-spaces-before: 0
+ max-spaces-after: 1
+ commas:
+ level: error
+ max-spaces-before: 0
+ min-spaces-after: 1
+ max-spaces-after: 1
+ comments:
+ level: error
+ require-starting-space: true
+ min-spaces-from-content: 2
+ comments-indentation:
+ level: error
+ document-end:
+ level: error
+ present: false
+ document-start:
+ level: error
+ present: false
+ empty-lines:
+ level: error
+ max: 1
+ max-start: 0
+ max-end: 1
+ hyphens:
+ level: error
+ max-spaces-after: 1
+ indentation:
+ level: error
+ spaces: 2
+ indent-sequences: true
+ check-multi-line-strings: false
+ key-duplicates:
+ level: error
+ line-length: disable
+ new-line-at-end-of-file:
+ level: error
+ new-lines:
+ level: error
+ type: unix
+ trailing-spaces:
+ level: error
+ truthy:
+ disable
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b044e0f..6b75518 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,61 +1,61 @@
-# Contribution guidelines
-
-Contributing to this project should be as easy and transparent as possible, whether it's:
-
-- Reporting a bug
-- Discussing the current state of the code
-- Submitting a fix
-- Proposing new features
-
-## Github is used for everything
-
-Github is used to host code, to track issues and feature requests, as well as accept pull requests.
-
-Pull requests are the best way to propose changes to the codebase.
-
-1. Fork the repo and create your branch from `master`.
-2. If you've changed something, update the documentation.
-3. Make sure your code lints (using black).
-4. Test you contribution.
-5. Issue that pull request!
-
-## Any contributions you make will be under project license
-
-In short, when you submit code changes, your submissions are understood to be under the same [License](../LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern.
-
-## Report bugs using Github's [issues](../../issues)
-
-GitHub issues are used to track public bugs.
-Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
-
-## Write bug reports with detail, background, and sample code
-
-**Great Bug Reports** tend to have:
-
-- A quick summary and/or background
-- Steps to reproduce
- - Be specific!
- - Give sample code if you can.
-- What you expected would happen
-- What actually happens
-- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
-
-People _love_ thorough bug reports. I'm not even kidding.
-
-## Use a Consistent Coding Style
-
-Use [black](https://github.com/ambv/black) to make sure the code follows the style.
-
-## Test your code modification
-
-This custom component is based on [integration_blueprint template](https://github.com/custom-components/integration_blueprint).
-
-It comes with development environment in a container, easy to launch
-if you use Visual Studio Code. With this container you will have a stand alone
-Home Assistant instance running and already configured with the included
-[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
-file.
-
-## License
-
-By contributing, you agree that your contributions will be licensed under the projects License.
+# Contribution guidelines
+
+Contributing to this project should be as easy and transparent as possible, whether it's:
+
+- Reporting a bug
+- Discussing the current state of the code
+- Submitting a fix
+- Proposing new features
+
+## Github is used for everything
+
+Github is used to host code, to track issues and feature requests, as well as accept pull requests.
+
+Pull requests are the best way to propose changes to the codebase.
+
+1. Fork the repo and create your branch from `master`.
+2. If you've changed something, update the documentation.
+3. Make sure your code lints (using black).
+4. Test you contribution.
+5. Issue that pull request!
+
+## Any contributions you make will be under project license
+
+In short, when you submit code changes, your submissions are understood to be under the same [License](../LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern.
+
+## Report bugs using Github's [issues](../../issues)
+
+GitHub issues are used to track public bugs.
+Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
+
+## Write bug reports with detail, background, and sample code
+
+**Great Bug Reports** tend to have:
+
+- A quick summary and/or background
+- Steps to reproduce
+ - Be specific!
+ - Give sample code if you can.
+- What you expected would happen
+- What actually happens
+- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
+
+People _love_ thorough bug reports. I'm not even kidding.
+
+## Use a Consistent Coding Style
+
+Use [black](https://github.com/ambv/black) to make sure the code follows the style.
+
+## Test your code modification
+
+This custom component is based on [integration_blueprint template](https://github.com/custom-components/integration_blueprint).
+
+It comes with development environment in a container, easy to launch
+if you use Visual Studio Code. With this container you will have a stand alone
+Home Assistant instance running and already configured with the included
+[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
+file.
+
+## License
+
+By contributing, you agree that your contributions will be licensed under the projects License.
diff --git a/README.md b/README.md
index bb42f8d..1c34245 100644
--- a/README.md
+++ b/README.md
@@ -38,14 +38,15 @@ With this Home Assistant Custom Component you'll be able to
### Installation
To add your Ecovacs devices into your Home Assistant:
- 1. Install [HACS](https://hacs.xyz)
- 2. In HACS: Go to Integrations and search for -> Deebot for Home Assistant <- and install
- 3. Deebot for Home Assistant is now available in Home Assistant under Settings -> Integration -> Add -> Deebot for Home Assistant
- 4. Configure as described below
+
+1. Install [HACS](https://hacs.xyz)
+2. In HACS: Go to Integrations and search for -> Deebot for Home Assistant <- and install
+3. Deebot for Home Assistant is now available in Home Assistant under Settings -> Integration -> Add -> Deebot for Home Assistant
+4. Configure as described below
### 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.
+For chinese server username you require "short id" and password. short id look like "EXXXXXX". DO NOT USE YOUR MOBILE PHONE NUMBER, it won't work.
country: cn
continent: as (or ww)
@@ -106,7 +107,7 @@ Example for fan_speed:
{{ states.vacuum.YOUR_ROBOT_NAME.attributes['fan_speed'] }}
```
-Get room numbers dynamically, very helpfull if your robot is multi-floor or if your robot lose the map and you don't want to change automations every time:
+Get room numbers dynamically, very helpful if your robot is multi-floor or if your robot lose the map and you don't want to change automations every time:
```
{{ states.vacuum.YOURROBOTNAME.attributes.room_bathroom }}
diff --git a/bandit.yaml b/bandit.yaml
new file mode 100644
index 0000000..568f77d
--- /dev/null
+++ b/bandit.yaml
@@ -0,0 +1,21 @@
+# https://bandit.readthedocs.io/en/latest/config.html
+
+tests:
+ - B103
+ - B108
+ - B306
+ - B307
+ - B313
+ - B314
+ - B315
+ - B316
+ - B317
+ - B318
+ - B319
+ - B320
+ - B325
+ - B601
+ - B602
+ - B604
+ - B608
+ - B609
diff --git a/custom_components/deebot/__init__.py b/custom_components/deebot/__init__.py
index 8f05a22..48d1716 100644
--- a/custom_components/deebot/__init__.py
+++ b/custom_components/deebot/__init__.py
@@ -1,15 +1,22 @@
"""Support for Deebot Vaccums."""
import asyncio
import logging
+from typing import Any, Dict
from awesomeversion import AwesomeVersion
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_DEVICES, CONF_VERIFY_SSL, CONF_USERNAME
+from homeassistant.const import CONF_DEVICES, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import HomeAssistant
from . import hub
-from .const import DOMAIN, STARTUP_MESSAGE, CONF_BUMPER, CONF_CLIENT_DEVICE_ID, MIN_REQUIRED_HA_VERSION
+from .const import (
+ CONF_BUMPER,
+ CONF_CLIENT_DEVICE_ID,
+ DOMAIN,
+ MIN_REQUIRED_HA_VERSION,
+ STARTUP_MESSAGE,
+)
from .helpers import get_bumper_device_id
_LOGGER = logging.getLogger(__name__)
@@ -18,10 +25,14 @@
def is_ha_supported() -> bool:
+ """Return True, if current HA version is supported."""
if AwesomeVersion(HA_VERSION) >= MIN_REQUIRED_HA_VERSION:
return True
- _LOGGER.error(f"Unsupported HA version! Please upgrade home assistant at least to \"{MIN_REQUIRED_HA_VERSION}\"")
+ _LOGGER.error(
+ 'Unsupported HA version! Please upgrade home assistant at least to "%s"',
+ MIN_REQUIRED_HA_VERSION,
+ )
return False
@@ -45,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""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
@@ -68,13 +79,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return unload_ok
-async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
- _LOGGER.debug(f"Migrating from version {config_entry.version}")
+ _LOGGER.debug("Migrating from version %d", config_entry.version)
if config_entry.version == 1:
- new = {**config_entry.data,
- CONF_VERIFY_SSL: True}
+ new: Dict[str, Any] = {**config_entry.data, CONF_VERIFY_SSL: True}
device_id = "deviceid"
devices = new.pop(device_id, {})
@@ -95,6 +105,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
config_entry.data = {**new}
config_entry.version = 3
- _LOGGER.info(f"Migration to version {config_entry.version} successful")
+ _LOGGER.info("Migration to version %d successful", config_entry.version)
return True
diff --git a/custom_components/deebot/binary_sensor.py b/custom_components/deebot/binary_sensor.py
index 6b34b0d..360274e 100644
--- a/custom_components/deebot/binary_sensor.py
+++ b/custom_components/deebot/binary_sensor.py
@@ -1,10 +1,14 @@
-"""Support for Deebot Sensor."""
+"""Binary sensor module."""
import logging
-from typing import Optional, Dict, Any
+from typing import Any, Dict, Optional
-from deebotozmo.events import WaterInfoEvent, EventListener
+from deebotozmo.event_emitter import EventListener
+from deebotozmo.events import WaterInfoEvent
from deebotozmo.vacuum_bot import VacuumBot
from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .helpers import get_device_info
@@ -13,7 +17,11 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, config_entry, async_add_devices):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Add binary_sensor for passed config_entry in HA."""
hub: DeebotHub = hass.data[DOMAIN][config_entry.entry_id]
@@ -22,11 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
new_devices.append(DeebotMopAttachedBinarySensor(vacbot, "mop_attached"))
if new_devices:
- async_add_devices(new_devices)
+ async_add_entities(new_devices)
-class DeebotMopAttachedBinarySensor(BinarySensorEntity):
- """Deebot mop attached binary sensor"""
+class DeebotMopAttachedBinarySensor(BinarySensorEntity): # type: ignore
+ """Deebot mop attached binary sensor."""
_attr_should_poll = False
_attr_entity_registry_enabled_default = False
@@ -51,15 +59,16 @@ def icon(self) -> Optional[str]:
@property
def device_info(self) -> Optional[Dict[str, Any]]:
+ """Return device specific attributes."""
return get_device_info(self._vacuum_bot)
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(event: WaterInfoEvent):
- self._attr_is_on = event.mopAttached
+ async def on_event(event: WaterInfoEvent) -> None:
+ self._attr_is_on = event.mop_attached
self.async_write_ha_state()
- listener: EventListener = self._vacuum_bot.waterEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.water_info.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
diff --git a/custom_components/deebot/camera.py b/custom_components/deebot/camera.py
index 6f68a4f..32abb74 100644
--- a/custom_components/deebot/camera.py
+++ b/custom_components/deebot/camera.py
@@ -1,20 +1,28 @@
"""Support for Deebot Vaccums."""
import base64
import logging
-from typing import Optional, Dict, Any
+from typing import Any, Dict, Optional
-from deebotozmo.events import EventListener, MapEvent
+from deebotozmo.event_emitter import EventListener
+from deebotozmo.events import MapEvent
from deebotozmo.vacuum_bot import VacuumBot
from homeassistant.components.camera import Camera
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from .const import *
+from .const import DOMAIN
from .helpers import get_device_info
from .hub import DeebotHub
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, config_entry, async_add_devices):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Add sensors for passed config_entry in HA."""
hub: DeebotHub = hass.data[DOMAIN][config_entry.entry_id]
@@ -24,11 +32,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
new_devices.append(DeeboLiveCamera(vacbot, "liveMap"))
if new_devices:
- async_add_devices(new_devices)
+ async_add_entities(new_devices)
-class DeeboLiveCamera(Camera):
- """Deebot Live Camera"""
+class DeeboLiveCamera(Camera): # type: ignore
+ """Deebot Live Camera."""
_attr_entity_registry_enabled_default = False
@@ -48,11 +56,16 @@ def __init__(self, vacuum_bot: VacuumBot, device_id: str):
@property
def device_info(self) -> Optional[Dict[str, Any]]:
+ """Return device specific attributes."""
return get_device_info(self._vacuum_bot)
- async def async_camera_image(self, width: Optional[int] = None, height: Optional[int] = None) -> Optional[bytes]:
+ async def async_camera_image(
+ self, width: Optional[int] = None, _: Optional[int] = None
+ ) -> Optional[bytes]:
"""Return a still image response from the camera.
- Integrations may choose to ignore the height parameter in order to preserve aspect ratio"""
+
+ Integrations may choose to ignore the height parameter in order to preserve aspect ratio
+ """
return base64.decodebytes(self._vacuum_bot.map.get_base64_map(width))
@@ -60,8 +73,8 @@ async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(_: MapEvent):
+ async def on_event(_: MapEvent) -> None:
self.schedule_update_ha_state()
- listener: EventListener = self._vacuum_bot.map.mapEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.map.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
diff --git a/custom_components/deebot/config_flow.py b/custom_components/deebot/config_flow.py
index 8a184b2..75dda38 100644
--- a/custom_components/deebot/config_flow.py
+++ b/custom_components/deebot/config_flow.py
@@ -2,17 +2,34 @@
import logging
import random
import string
+from typing import Any, Dict, List, Optional
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from aiohttp import ClientError
from deebotozmo.ecovacs_api import EcovacsAPI
+from deebotozmo.models import Vacuum
from deebotozmo.util import md5
from homeassistant import config_entries
-from homeassistant.const import CONF_MODE, CONF_DEVICES
+from homeassistant.const import (
+ CONF_DEVICES,
+ CONF_MODE,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
-from .const import *
+from .const import (
+ BUMPER_CONFIGURATION,
+ CONF_CLIENT_DEVICE_ID,
+ CONF_CONTINENT,
+ CONF_COUNTRY,
+ CONF_MODE_BUMPER,
+ CONF_MODE_CLOUD,
+ DOMAIN,
+)
from .helpers import get_bumper_device_id
_LOGGER = logging.getLogger(__name__)
@@ -32,32 +49,34 @@
)
-class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # type: ignore
"""Handle a config flow for Deebot."""
VERSION = 3
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self) -> None:
- self.data = {}
- self.robot_list = []
- self.mode = None
+ self._data: Dict[str, Any] = {}
+ self._robot_list: List[Vacuum] = []
+ self._mode: Optional[str] = None
- async def async_retrieve_bots(self, domain_config: dict):
+ async def _async_retrieve_bots(self, domain_config: Dict[str, Any]) -> List[Vacuum]:
ecovacs_api = EcovacsAPI(
aiohttp_client.async_get_clientsession(self.hass),
DEEBOT_API_DEVICEID,
- domain_config.get(CONF_USERNAME),
- md5(domain_config.get(CONF_PASSWORD)),
- continent=domain_config.get(CONF_CONTINENT),
- country=domain_config.get(CONF_COUNTRY),
- verify_ssl=domain_config.get(CONF_VERIFY_SSL, True)
+ domain_config[CONF_USERNAME],
+ md5(domain_config[CONF_PASSWORD]),
+ continent=domain_config[CONF_CONTINENT],
+ country=domain_config[CONF_COUNTRY],
+ verify_ssl=domain_config.get(CONF_VERIFY_SSL, True),
)
await ecovacs_api.login()
return await ecovacs_api.get_devices()
- async def async_step_user(self, user_input=None):
+ async def async_step_user(
+ self, user_input: Optional[Dict[str, Any]] = None
+ ) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
@@ -68,37 +87,39 @@ async def async_step_user(self, user_input=None):
errors[CONF_CONTINENT] = "invalid_continent"
try:
- info = await self.async_retrieve_bots(user_input)
- self.robot_list = info
- except ClientError as e:
- _LOGGER.debug("Cannot connect", e, exc_info=True)
+ info = await self._async_retrieve_bots(user_input)
+ self._robot_list = info
+ except ClientError:
+ _LOGGER.debug("Cannot connect", exc_info=True)
errors["base"] = "cannot_connect"
- except ValueError as e:
- _LOGGER.debug("Invalid auth", e, exc_info=True)
+ except ValueError:
+ _LOGGER.debug("Invalid auth", exc_info=True)
errors["base"] = "invalid_auth"
- except Exception as e:
- _LOGGER.error("Unexpected exception", e, exc_info=True)
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.error("Unexcdepted exception", exc_info=True)
errors["base"] = "unknown"
if not errors:
- self.data.update(user_input)
+ self._data.update(user_input)
return await self.async_step_robots()
- if self.show_advanced_options and self.mode is None:
+ if self.show_advanced_options and self._mode is None:
return await self.async_step_user_advanced()
return self.async_show_form(
step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors
)
- async def async_step_user_advanced(self, user_input=None):
+ async def async_step_user_advanced(
+ self, user_input: Optional[Dict[str, Any]] = None
+ ) -> FlowResult:
"""Handle an advanced mode flow initialized by the user."""
if user_input is not None:
- self.mode = user_input.get(CONF_MODE, CONF_MODE_CLOUD)
- if self.mode == CONF_MODE_BUMPER:
+ self._mode = user_input.get(CONF_MODE, CONF_MODE_CLOUD)
+ if self._mode == CONF_MODE_BUMPER:
config = {
**BUMPER_CONFIGURATION,
- CONF_CLIENT_DEVICE_ID: get_bumper_device_id(self.hass)
+ CONF_CLIENT_DEVICE_ID: get_bumper_device_id(self.hass),
}
return await self.async_step_user(user_input=config)
@@ -112,11 +133,11 @@ async def async_step_user_advanced(self, user_input=None):
}
)
- return self.async_show_form(
- step_id="user_advanced", data_schema=data_schema
- )
+ return self.async_show_form(step_id="user_advanced", data_schema=data_schema)
- async def async_step_robots(self, user_input=None):
+ async def async_step_robots(
+ self, user_input: Optional[Dict[str, Any]] = None
+ ) -> FlowResult:
"""Handle the robots selection step."""
errors = {}
@@ -125,21 +146,23 @@ async def async_step_robots(self, user_input=None):
if len(user_input[CONF_DEVICES]) < 1:
errors["base"] = "select_robots"
else:
- self.data.update(user_input)
+ self._data.update(user_input)
return self.async_create_entry(
- title=self.data[CONF_USERNAME], data=self.data
+ title=self._data[CONF_USERNAME], data=self._data
)
- except Exception as e:
- _LOGGER.error("Unexpected exception", e, exc_info=True)
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.error("Unexpected exception", exc_info=True)
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.get("nick", e["name"]) for e in self.robot_list}
+ robot_list_dict = {
+ e["name"]: e.get("nick", e["name"]) for e in self._robot_list
+ }
options_schema = vol.Schema(
{
vol.Required(
- CONF_DEVICES, default=list(robot_listDict.keys())
- ): cv.multi_select(robot_listDict)
+ CONF_DEVICES, default=list(robot_list_dict.keys())
+ ): cv.multi_select(robot_list_dict)
}
)
diff --git a/custom_components/deebot/const.py b/custom_components/deebot/const.py
index 83c14aa..4263db6 100644
--- a/custom_components/deebot/const.py
+++ b/custom_components/deebot/const.py
@@ -1,3 +1,4 @@
+"""Const module."""
from deebotozmo.models import VacuumState
from homeassistant.components.vacuum import (
STATE_CLEANING,
@@ -42,7 +43,7 @@
CONF_COUNTRY: "it",
CONF_PASSWORD: CONF_BUMPER,
CONF_USERNAME: CONF_BUMPER,
- CONF_VERIFY_SSL: False # required as bumper is using self signed certificates
+ CONF_VERIFY_SSL: False, # required as bumper is using self signed certificates
}
DEEBOT_DEVICES = f"{DOMAIN}_devices"
diff --git a/custom_components/deebot/helpers.py b/custom_components/deebot/helpers.py
index 659413c..9ac58e1 100644
--- a/custom_components/deebot/helpers.py
+++ b/custom_components/deebot/helpers.py
@@ -1,4 +1,5 @@
-from typing import Optional, Dict
+"""Helpers module."""
+from typing import Dict, Optional
from deebotozmo.models import Vacuum
from deebotozmo.vacuum_bot import VacuumBot
@@ -9,6 +10,7 @@
def get_device_info(vacuum_bot: VacuumBot) -> Optional[Dict]:
+ """Return device info for given vacuum."""
device: Vacuum = vacuum_bot.vacuum
identifiers = set()
if device.did:
@@ -30,8 +32,9 @@ def get_device_info(vacuum_bot: VacuumBot) -> Optional[Dict]:
def get_bumper_device_id(hass: HomeAssistant) -> str:
+ """Return bumper device id."""
try:
- location_name = hass.config.location_name.strip().replace(' ', '_')
- except:
+ location_name = hass.config.location_name.strip().replace(" ", "_")
+ except Exception: # pylint: disable=broad-except
location_name = ""
return f"Deebot-4-HA_{location_name}_{uuid.random_uuid_hex()[:4]}"
diff --git a/custom_components/deebot/hub.py b/custom_components/deebot/hub.py
index b44c601..70299f1 100644
--- a/custom_components/deebot/hub.py
+++ b/custom_components/deebot/hub.py
@@ -1,8 +1,9 @@
+"""Hub module."""
import asyncio
import logging
import random
import string
-from typing import Any, Mapping, Optional
+from typing import Any, List, Mapping, Optional
import aiohttp
from aiohttp import ClientError
@@ -10,30 +11,34 @@
from deebotozmo.ecovacs_mqtt import EcovacsMqtt
from deebotozmo.util import md5
from deebotozmo.vacuum_bot import VacuumBot
-from homeassistant.const import CONF_DEVICES
+from homeassistant.const import (
+ CONF_DEVICES,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
-from .const import *
+from .const import CONF_CLIENT_DEVICE_ID, CONF_CONTINENT, CONF_COUNTRY
_LOGGER = logging.getLogger(__name__)
class DeebotHub:
- """Deebot Hub"""
+ """Deebot Hub."""
def __init__(self, hass: HomeAssistant, config: Mapping[str, Any]):
- """Initialize the Deebot Vacuum."""
-
self._config: Mapping[str, Any] = config
self._hass: HomeAssistant = hass
- self._country: str = config.get(CONF_COUNTRY).lower()
- self._continent: str = config.get(CONF_CONTINENT).lower()
- self.vacuum_bots: [VacuumBot] = []
+ self._country: str = config.get(CONF_COUNTRY, "it").lower()
+ self._continent: str = config.get(CONF_CONTINENT, "eu").lower()
+ self.vacuum_bots: List[VacuumBot] = []
self._verify_ssl = config.get(CONF_VERIFY_SSL, True)
- self._session: aiohttp.ClientSession = aiohttp_client.async_get_clientsession(self._hass,
- verify_ssl=self._verify_ssl)
+ self._session: aiohttp.ClientSession = aiohttp_client.async_get_clientsession(
+ self._hass, verify_ssl=self._verify_ssl
+ )
device_id = config.get(CONF_CLIENT_DEVICE_ID)
@@ -47,68 +52,76 @@ def __init__(self, hass: HomeAssistant, config: Mapping[str, Any]):
self._ecovacs_api = EcovacsAPI(
self._session,
device_id,
- config.get(CONF_USERNAME),
- md5(config.get(CONF_PASSWORD)),
+ config.get(CONF_USERNAME, ""),
+ md5(config.get(CONF_PASSWORD, "")),
continent=self._continent,
country=self._country,
- verify_ssl=self._verify_ssl
+ verify_ssl=self._verify_ssl,
)
- async def async_setup(self):
+ async def async_setup(self) -> None:
+ """Init hub."""
try:
await self._ecovacs_api.login()
auth = await self._ecovacs_api.get_request_auth()
- self._mqtt = EcovacsMqtt(auth, continent=self._continent, country=self._country)
+ self._mqtt = EcovacsMqtt(
+ auth, continent=self._continent, country=self._country
+ )
devices = await self._ecovacs_api.get_devices()
# CREATE VACBOT FOR EACH DEVICE
for device in devices:
- if device["name"] in self._config.get(CONF_DEVICES):
+ if device["name"] in self._config.get(CONF_DEVICES, []):
vacbot = VacuumBot(
self._session,
auth,
device,
continent=self._continent,
country=self._country,
- verify_ssl=self._verify_ssl
+ verify_ssl=self._verify_ssl,
)
await self._mqtt.subscribe(vacbot)
- _LOGGER.debug("New vacbot found: " + device["name"])
+ _LOGGER.debug("New vacbot found: %s", device["name"])
self.vacuum_bots.append(vacbot)
asyncio.create_task(self._check_status_task())
_LOGGER.debug("Hub setup complete")
- except Exception as e:
+ except Exception as ex:
msg = "Error during setup"
- _LOGGER.error(msg, e, exc_info=True)
- raise ConfigEntryNotReady(msg) from e
+ _LOGGER.error(msg, exc_info=True)
+ raise ConfigEntryNotReady(msg) from ex
def disconnect(self) -> None:
- self._mqtt.disconnect()
+ """Disconnect hub."""
+ if self._mqtt:
+ self._mqtt.disconnect()
@property
- def name(self):
- """ Return the name of the hub."""
+ def name(self) -> str:
+ """Return the name of the hub."""
return "Deebot Hub"
- async def _check_status_task(self):
+ async def _check_status_task(self) -> None:
while True:
try:
await asyncio.sleep(60)
await self._check_status_function()
- except ClientError as e:
- _LOGGER.warning(f"A client error occurred, probably the ecovacs servers are unstable: {e}")
- except Exception as e:
- _LOGGER.error(f"Unknown exception occurred: {e}")
-
- async def _check_status_function(self):
+ except ClientError as ex:
+ _LOGGER.warning(
+ "A client error occurred, probably the ecovacs servers are unstable: %s",
+ ex,
+ )
+ except Exception as ex: # pylint: disable=broad-except
+ _LOGGER.error(ex, exc_info=True)
+
+ async def _check_status_function(self) -> None:
devices = await self._ecovacs_api.get_devices()
for device in devices:
bot: VacuumBot
for bot in self.vacuum_bots:
if device.did == bot.vacuum.did:
- bot.set_available(True if device.status == 1 else False)
+ bot.set_available(device.status == 1)
diff --git a/custom_components/deebot/manifest.json b/custom_components/deebot/manifest.json
index 04d05e0..ddab313 100644
--- a/custom_components/deebot/manifest.json
+++ b/custom_components/deebot/manifest.json
@@ -5,9 +5,7 @@
"config_flow": true,
"documentation": "https://github.com/And3rsL/Deebot-for-Home-Assistant",
"issue_tracker": "https://github.com/And3rsL/Deebot-for-Home-Assistant/issues",
- "requirements": [
- "deebotozmo==2.1.2"
- ],
+ "requirements": ["deebotozmo==3.0.0b1"],
"codeowners": ["@And3rsL", "@edenhaus"],
"iot_class": "cloud_polling"
}
diff --git a/custom_components/deebot/sensor.py b/custom_components/deebot/sensor.py
index 50aeec8..607eca5 100644
--- a/custom_components/deebot/sensor.py
+++ b/custom_components/deebot/sensor.py
@@ -1,13 +1,26 @@
-"""Support for Deebot Sensor."""
+"""Sensor module."""
import logging
-from typing import Optional, Dict, Any
-
-from deebotozmo.constants import COMPONENT_MAIN_BRUSH, COMPONENT_SIDE_BRUSH, COMPONENT_FILTER
-from deebotozmo.events import CleanLogEvent, WaterInfoEvent, LifeSpanEvent, StatsEvent, EventListener, ErrorEvent, \
- StatusEvent
+from typing import Any, Dict, Optional
+
+from deebotozmo.constants import (
+ COMPONENT_FILTER,
+ COMPONENT_MAIN_BRUSH,
+ COMPONENT_SIDE_BRUSH,
+)
+from deebotozmo.event_emitter import EventListener
+from deebotozmo.events import (
+ CleanLogEvent,
+ ErrorEvent,
+ StatsEvent,
+ StatusEvent,
+ WaterInfoEvent,
+)
from deebotozmo.vacuum_bot import VacuumBot
from homeassistant.components.sensor import SensorEntity
-from homeassistant.const import STATE_UNKNOWN, CONF_DESCRIPTION
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_DESCRIPTION, STATE_UNKNOWN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, LAST_ERROR
from .helpers import get_device_info
@@ -16,7 +29,11 @@
_LOGGER = logging.getLogger(__name__)
-async def async_setup_entry(hass, config_entry, async_add_devices):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Add sensors for passed config_entry in HA."""
hub: DeebotHub = hass.data[DOMAIN][config_entry.entry_id]
@@ -40,11 +57,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
new_devices.append(DeebotStatsSensor(vacbot, "start"))
if new_devices:
- async_add_devices(new_devices)
+ async_add_entities(new_devices)
-class DeebotBaseSensor(SensorEntity):
- """Deebot base sensor"""
+class DeebotBaseSensor(SensorEntity): # type: ignore
+ """Deebot base sensor."""
_attr_should_poll = False
_attr_entity_registry_enabled_default = False
@@ -64,113 +81,117 @@ def __init__(self, vacuum_bot: VacuumBot, device_id: str):
@property
def device_info(self) -> Optional[Dict[str, Any]]:
+ """Return device specific attributes."""
return get_device_info(self._vacuum_bot)
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(event: StatusEvent):
+ async def on_event(event: StatusEvent) -> None:
if not event.available:
self._attr_native_value = STATE_UNKNOWN
self.async_write_ha_state()
- listener: EventListener = self._vacuum_bot.statusEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.status.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
class DeebotLastCleanImageSensor(DeebotBaseSensor):
- """Deebot Sensor"""
+ """Deebot last clean image sensor."""
_attr_icon = "mdi:image-search"
def __init__(self, vacuum_bot: VacuumBot):
"""Initialize the Sensor."""
- super(DeebotLastCleanImageSensor, self).__init__(vacuum_bot, "last_clean_image")
+ super().__init__(vacuum_bot, "last_clean_image")
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(event: CleanLogEvent):
+ async def on_event(event: CleanLogEvent) -> None:
if event.logs:
- self._attr_native_value = event.logs[0].imageUrl
+ self._attr_native_value = event.logs[0].image_url
else:
self._attr_native_value = STATE_UNKNOWN
self.async_write_ha_state()
- listener: EventListener = self._vacuum_bot.cleanLogsEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.clean_logs.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
class DeebotWaterLevelSensor(DeebotBaseSensor):
- """Deebot Sensor"""
+ """Deebot water level sensor."""
_attr_icon = "mdi:water"
def __init__(self, vacuum_bot: VacuumBot):
"""Initialize the Sensor."""
- super(DeebotWaterLevelSensor, self).__init__(vacuum_bot, "water_level")
+ super().__init__(vacuum_bot, "water_level")
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(event: WaterInfoEvent):
+ async def on_event(event: WaterInfoEvent) -> None:
if event.amount:
self._attr_native_value = event.amount
self.async_write_ha_state()
- listener: EventListener = self._vacuum_bot.waterEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.water_info.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
class DeebotComponentSensor(DeebotBaseSensor):
- """Deebot Sensor"""
+ """Deebot component sensor."""
_attr_native_unit_of_measurement = "%"
def __init__(self, vacuum_bot: VacuumBot, device_id: str):
"""Initialize the Sensor."""
- super(DeebotComponentSensor, self).__init__(vacuum_bot, device_id)
- self._attr_icon = "mdi:air-filter" if device_id == COMPONENT_FILTER else "mdi:broom"
+ super().__init__(vacuum_bot, device_id)
+ self._attr_icon = (
+ "mdi:air-filter" if device_id == COMPONENT_FILTER else "mdi:broom"
+ )
self._id = device_id
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(event: LifeSpanEvent):
- if self._id == event.type:
- self._attr_native_value = event.percent
+ async def on_event(event: Dict[str, float]) -> None:
+ value = event.get(self._id, None)
+ if value:
+ self._attr_native_value = value
self.async_write_ha_state()
- listener: EventListener = self._vacuum_bot.lifespanEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.lifespan.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
class DeebotStatsSensor(DeebotBaseSensor):
- """Deebot Sensor"""
+ """Deebot stats sensor."""
- def __init__(self, vacuum_bot: VacuumBot, type: str):
+ def __init__(self, vacuum_bot: VacuumBot, stats_type: str):
"""Initialize the Sensor."""
- super(DeebotStatsSensor, self).__init__(vacuum_bot, f"stats_{type}")
- self._type = type
- if type == "area":
+ super().__init__(vacuum_bot, f"stats_{stats_type}")
+ self._type = stats_type
+ if stats_type == "area":
self._attr_icon = "mdi:floor-plan"
self._attr_native_unit_of_measurement = "mq"
- elif type == "time":
+ elif stats_type == "time":
self._attr_icon = "mdi:timer-outline"
self._attr_native_unit_of_measurement = "min"
- elif type == "type":
+ elif stats_type == "type":
self._attr_icon = "mdi:cog"
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(event: StatsEvent):
+ async def on_event(event: StatsEvent) -> None:
if hasattr(event, self._type):
value = getattr(event, self._type)
@@ -184,29 +205,27 @@ async def on_event(event: StatsEvent):
self.async_write_ha_state()
- listener: EventListener = self._vacuum_bot.statsEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.stats.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
class DeebotLastErrorSensor(DeebotBaseSensor):
- """Deebot Sensor"""
+ """Deebot last error sensor."""
_attr_icon = "mdi:alert-circle"
def __init__(self, vacuum_bot: VacuumBot):
"""Initialize the Sensor."""
- super(DeebotLastErrorSensor, self).__init__(vacuum_bot, LAST_ERROR)
+ super().__init__(vacuum_bot, LAST_ERROR)
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_event(event: ErrorEvent):
+ async def on_event(event: ErrorEvent) -> None:
self._attr_native_value = event.code
- self._attr_extra_state_attributes = {
- CONF_DESCRIPTION: event.description
- }
+ self._attr_extra_state_attributes = {CONF_DESCRIPTION: event.description}
self.async_write_ha_state()
- listener: EventListener = self._vacuum_bot.errorEvents.subscribe(on_event)
+ listener: EventListener = self._vacuum_bot.events.error.subscribe(on_event)
self.async_on_remove(listener.unsubscribe)
diff --git a/custom_components/deebot/services.yaml b/custom_components/deebot/services.yaml
index cf240c7..d1f46c0 100644
--- a/custom_components/deebot/services.yaml
+++ b/custom_components/deebot/services.yaml
@@ -1,4 +1,5 @@
-refresh: # Must be kept in sync with vacuum.py
+# Must be kept in sync with vacuum.py
+refresh:
name: Manually refresh
description: Manually request a refresh
target:
diff --git a/custom_components/deebot/translations/de.json b/custom_components/deebot/translations/de.json
index 13e9023..db00622 100644
--- a/custom_components/deebot/translations/de.json
+++ b/custom_components/deebot/translations/de.json
@@ -1,33 +1,33 @@
{
- "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"
- }
- },
- "robots": {
- "data": {
- "devices": "Geräte"
- }
- },
- "user_advanced": {
- "description": "Wählen Sie \"Bumper\" nur, falls Sie eine funktionierende Bumper-Instanz haben. Sonst wählen Sie bitte die empfohlene Variante \"Cloud\"."
- }
+ "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"
}
- }
-}
\ No newline at end of file
+ },
+ "robots": {
+ "data": {
+ "devices": "Geräte"
+ }
+ },
+ "user_advanced": {
+ "description": "Wählen Sie \"Bumper\" nur, falls Sie eine funktionierende Bumper-Instanz haben. Sonst wählen Sie bitte die empfohlene Variante \"Cloud\"."
+ }
+ }
+ }
+}
diff --git a/custom_components/deebot/translations/en.json b/custom_components/deebot/translations/en.json
index 7a1b0aa..f357d40 100644
--- a/custom_components/deebot/translations/en.json
+++ b/custom_components/deebot/translations/en.json
@@ -1,33 +1,33 @@
{
- "config": {
- "abort": {
- "already_configured": "Already 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"
- }
- },
- "robots": {
- "data": {
- "devices": "Devices"
- }
- },
- "user_advanced": {
- "description": "Please select \"Bumper\" ONLY if you have a working bumper instance already. Otherwise, select \"Cloud\" please."
- }
+ "config": {
+ "abort": {
+ "already_configured": "Already 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"
}
- }
+ },
+ "robots": {
+ "data": {
+ "devices": "Devices"
+ }
+ },
+ "user_advanced": {
+ "description": "Please select \"Bumper\" ONLY if you have a working bumper instance already. Otherwise, select \"Cloud\" please."
+ }
+ }
+ }
}
diff --git a/custom_components/deebot/translations/fr.json b/custom_components/deebot/translations/fr.json
index 0350dcc..fbbd56b 100644
--- a/custom_components/deebot/translations/fr.json
+++ b/custom_components/deebot/translations/fr.json
@@ -1,33 +1,33 @@
{
- "config": {
- "abort": {
- "already_configured": "Déjà configuré"
- },
- "error": {
- "cannot_connect": "Connection impossible à l'API ecovacs",
- "invalid_auth": "Nom d’utilisateur ou mot de passe incorrect",
- "unknown": "Erreur inconnue",
- "invalid_country":"Le code du pays doit être en deux lettres, ex: fr, be, us, etc ",
- "invalid_continent":"Le code du continent doit être en deux lettres, ex: eu, ww, etc ",
- "select_robots": "Sélectionnez au moins 1 robot"
- },
- "step": {
- "user": {
- "data": {
- "password": "Mot de passe",
- "username": "E-mail ou Ecovacs ID",
- "country": "Pays",
- "continent": "Continent"
- }
- },
- "robots": {
- "data": {
- "devices": "Appareils"
- }
- },
- "user_advanced": {
- "description": "Sélectionnez \"Bumper\" SEULEMENT si vous avez déjà une instance bumper fonctionnelle. Sinon, sélectionnez \"Cloud\" s'il vous plait."
- }
+ "config": {
+ "abort": {
+ "already_configured": "Déjà configuré"
+ },
+ "error": {
+ "cannot_connect": "Connection impossible à l'API ecovacs",
+ "invalid_auth": "Nom d’utilisateur ou mot de passe incorrect",
+ "unknown": "Erreur inconnue",
+ "invalid_country": "Le code du pays doit être en deux lettres, ex: fr, be, us, etc ",
+ "invalid_continent": "Le code du continent doit être en deux lettres, ex: eu, ww, etc ",
+ "select_robots": "Sélectionnez au moins 1 robot"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "E-mail ou Ecovacs ID",
+ "country": "Pays",
+ "continent": "Continent"
}
- }
+ },
+ "robots": {
+ "data": {
+ "devices": "Appareils"
+ }
+ },
+ "user_advanced": {
+ "description": "Sélectionnez \"Bumper\" SEULEMENT si vous avez déjà une instance bumper fonctionnelle. Sinon, sélectionnez \"Cloud\" s'il vous plait."
+ }
+ }
+ }
}
diff --git a/custom_components/deebot/translations/it.json b/custom_components/deebot/translations/it.json
index 8bd9390..910228c 100644
--- a/custom_components/deebot/translations/it.json
+++ b/custom_components/deebot/translations/it.json
@@ -1,28 +1,28 @@
{
- "config": {
- "abort": {
- "already_configured": "Già configurato"
+ "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"
},
- "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"
- },
- "robots": {
- "devices": "Dispositivi"
- }
- }
+ "robots": {
+ "devices": "Dispositivi"
}
- }
-}
\ No newline at end of file
+ }
+ }
+ }
+}
diff --git a/custom_components/deebot/translations/zh-Hans.json b/custom_components/deebot/translations/zh-Hans.json
index c483dfd..e8a4fd2 100644
--- a/custom_components/deebot/translations/zh-Hans.json
+++ b/custom_components/deebot/translations/zh-Hans.json
@@ -1,27 +1,27 @@
{
- "config": {
- "abort": {
- "already_configured": "已存在现有配置"
- },
- "error": {
- "cannot_connect": "无法连接科沃斯服务器",
- "invalid_auth": "用户名或密码错误",
- "unknown": "未知错误",
- "invalid_country":"请输入正确的国家代码, 例如: us, uk, cn ",
- "invalid_continent":"请输入正确的地区代码, 例如: ww, eu, as ",
- "select_robots": "最少需要勾选一个机器人"
- },
- "step": {
- "user": {
- "data": {
- "password": "密码",
- "username": "电子邮件地址或科沃斯ID",
- "country": "国家",
- "continent": "地区",
- "live_map": "启用动态地图",
- "show_color_rooms": "显示房间的颜色 [BETA]"
- }
- }
+ "config": {
+ "abort": {
+ "already_configured": "已存在现有配置"
+ },
+ "error": {
+ "cannot_connect": "无法连接科沃斯服务器",
+ "invalid_auth": "用户名或密码错误",
+ "unknown": "未知错误",
+ "invalid_country": "请输入正确的国家代码, 例如: us, uk, cn ",
+ "invalid_continent": "请输入正确的地区代码, 例如: ww, eu, as ",
+ "select_robots": "最少需要勾选一个机器人"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "密码",
+ "username": "电子邮件地址或科沃斯ID",
+ "country": "国家",
+ "continent": "地区",
+ "live_map": "启用动态地图",
+ "show_color_rooms": "显示房间的颜色 [BETA]"
}
- }
+ }
+ }
+ }
}
diff --git a/custom_components/deebot/vacuum.py b/custom_components/deebot/vacuum.py
index 60ced6f..291bd31 100644
--- a/custom_components/deebot/vacuum.py
+++ b/custom_components/deebot/vacuum.py
@@ -1,37 +1,90 @@
"""Support for Deebot Vaccums."""
import logging
-from typing import Optional, Dict, Any, List, Mapping
+from typing import Any, Dict, List, Mapping, Optional
import voluptuous as vol
-from deebotozmo.commands import *
-from deebotozmo.constants import FAN_SPEED_QUIET, FAN_SPEED_NORMAL, FAN_SPEED_MAX, FAN_SPEED_MAXPLUS
-from deebotozmo.events import EventListener, BatteryEvent, RoomsEvent, FanSpeedEvent, StatusEvent, ErrorEvent
-from deebotozmo.models import Room
+from deebotozmo.commands import (
+ Charge,
+ CleanCustomArea,
+ CleanPause,
+ CleanResume,
+ CleanSpotArea,
+ CleanStart,
+ CleanStop,
+ Command,
+ PlaySound,
+ Relocate,
+ SetFanSpeed,
+ SetWaterLevel,
+)
+from deebotozmo.constants import (
+ FAN_SPEED_MAX,
+ FAN_SPEED_MAXPLUS,
+ FAN_SPEED_NORMAL,
+ FAN_SPEED_QUIET,
+)
+from deebotozmo.event_emitter import EventListener
+from deebotozmo.events import (
+ BatteryEvent,
+ ErrorEvent,
+ FanSpeedEvent,
+ RoomsEvent,
+ StatusEvent,
+)
+from deebotozmo.models import Room, VacuumState
from deebotozmo.vacuum_bot import VacuumBot
-from homeassistant.components.vacuum import SUPPORT_BATTERY, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, \
- SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_START, SUPPORT_STATE, SUPPORT_STOP, SUPPORT_MAP, \
- StateVacuumEntity
+from homeassistant.components.vacuum import (
+ SUPPORT_BATTERY,
+ SUPPORT_FAN_SPEED,
+ SUPPORT_LOCATE,
+ SUPPORT_MAP,
+ SUPPORT_PAUSE,
+ SUPPORT_RETURN_HOME,
+ SUPPORT_SEND_COMMAND,
+ SUPPORT_START,
+ SUPPORT_STATE,
+ SUPPORT_STOP,
+ StateVacuumEntity,
+)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
from homeassistant.util import slugify
-from .const import *
+from .const import (
+ DOMAIN,
+ EVENT_BATTERY,
+ EVENT_CLEAN_LOGS,
+ EVENT_ERROR,
+ EVENT_FAN_SPEED,
+ EVENT_LIFE_SPAN,
+ EVENT_MAP,
+ EVENT_ROOMS,
+ EVENT_STATS,
+ EVENT_STATUS,
+ EVENT_WATER,
+ LAST_ERROR,
+ VACUUMSTATE_TO_STATE,
+)
from .helpers import get_device_info
from .hub import DeebotHub
_LOGGER = logging.getLogger(__name__)
-SUPPORT_DEEBOT = (
- SUPPORT_PAUSE
- | SUPPORT_STOP
- | SUPPORT_RETURN_HOME
- | SUPPORT_FAN_SPEED
- | SUPPORT_BATTERY
- | SUPPORT_SEND_COMMAND
- | SUPPORT_LOCATE
- | SUPPORT_MAP
- | SUPPORT_STATE
- | SUPPORT_START
+SUPPORT_DEEBOT: int = (
+ SUPPORT_PAUSE
+ | SUPPORT_STOP
+ | SUPPORT_RETURN_HOME
+ | SUPPORT_FAN_SPEED
+ | SUPPORT_BATTERY
+ | SUPPORT_SEND_COMMAND
+ | SUPPORT_LOCATE
+ | SUPPORT_MAP
+ | SUPPORT_STATE
+ | SUPPORT_START
)
# Must be kept in sync with services.yaml
@@ -55,7 +108,11 @@
}
-async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_devices):
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
"""Add sensors for passed config_entry in HA."""
hub: DeebotHub = hass.data[DOMAIN][config_entry.entry_id]
@@ -64,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_devices
new_devices.append(DeebotVacuum(hass, vacbot))
if new_devices:
- async_add_devices(new_devices)
+ async_add_entities(new_devices)
platform = entity_platform.async_get_current_platform()
@@ -75,13 +132,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_devices
)
-def _unsubscribe_listeners(listeners: [EventListener]):
+def _unsubscribe_listeners(listeners: List[EventListener]) -> None:
for listener in listeners:
listener.unsubscribe()
-class DeebotVacuum(StateVacuumEntity):
- """Deebot Vacuums"""
+class DeebotVacuum(StateVacuumEntity): # type: ignore
+ """Deebot Vacuum."""
+
_attr_should_poll = False
def __init__(self, hass: HomeAssistant, vacuum_bot: VacuumBot):
@@ -96,7 +154,7 @@ def __init__(self, hass: HomeAssistant, vacuum_bot: VacuumBot):
name = self._device.vacuum.did
self._battery: Optional[int] = None
- self._fan_speed = None
+ self._fan_speed: Optional[str] = None
self._state: Optional[VacuumState] = None
self._rooms: List[Room] = []
self._last_error: Optional[ErrorEvent] = None
@@ -108,62 +166,59 @@ async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
- async def on_battery(event: BatteryEvent):
+ async def on_battery(event: BatteryEvent) -> None:
self._battery = event.value
self.async_write_ha_state()
- async def on_rooms(event: RoomsEvent):
+ async def on_rooms(event: RoomsEvent) -> None:
self._rooms = event.rooms
self.async_write_ha_state()
- async def on_fan_speed(event: FanSpeedEvent):
+ async def on_fan_speed(event: FanSpeedEvent) -> None:
self._fan_speed = event.speed
self.async_write_ha_state()
- async def on_status(event: StatusEvent):
+ async def on_status(event: StatusEvent) -> None:
self._attr_available = event.available
self._state = event.state
self.async_write_ha_state()
- async def on_error(event: ErrorEvent):
+ async def on_error(event: ErrorEvent) -> None:
self._last_error = event
self.async_write_ha_state()
- listeners = [
- self._device.statusEvents.subscribe(on_status),
- self._device.batteryEvents.subscribe(on_battery),
- self._device.map.roomsEvents.subscribe(on_rooms),
- self._device.fanSpeedEvents.subscribe(on_fan_speed),
- self._device.errorEvents.subscribe(on_error)
+ listeners: List[EventListener] = [
+ self._device.events.status.subscribe(on_status),
+ self._device.events.battery.subscribe(on_battery),
+ self._device.events.rooms.subscribe(on_rooms),
+ self._device.events.fan_speed.subscribe(on_fan_speed),
+ self._device.events.error.subscribe(on_error),
]
self.async_on_remove(lambda: _unsubscribe_listeners(listeners))
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag vacuum cleaner robot features that are supported."""
return SUPPORT_DEEBOT
@property
- def state(self):
+ def state(self) -> StateType:
"""Return the state of the vacuum cleaner."""
if self._state is not None and self.available:
return VACUUMSTATE_TO_STATE[self._state]
@property
- def battery_level(self):
+ def battery_level(self) -> Optional[int]:
"""Return the battery level of the vacuum cleaner."""
- if self._battery is not None:
- return self._battery
-
- return super().battery_level
+ return self._battery
@property
- def fan_speed(self):
+ def fan_speed(self) -> Optional[str]:
"""Return the fan speed of the vacuum cleaner."""
return self._fan_speed
@property
- def fan_speed_list(self):
+ def fan_speed_list(self) -> List[str]:
"""Get the list of available fan speed steps of the vacuum cleaner."""
return [FAN_SPEED_QUIET, FAN_SPEED_NORMAL, FAN_SPEED_MAX, FAN_SPEED_MAXPLUS]
@@ -174,7 +229,7 @@ def extra_state_attributes(self) -> Optional[Mapping[str, Any]]:
Implemented by platform classes. Convention for attribute names
is lowercase snake_case.
"""
- attributes = {}
+ attributes: Dict[str, Any] = {}
for room in self._rooms:
# convert room name to snake_case to meet the convention
room_name = "room_" + slugify(room.subtype)
@@ -188,81 +243,99 @@ def extra_state_attributes(self) -> Optional[Mapping[str, Any]]:
attributes[room_name] = [room_values, room.id]
if self._last_error:
- attributes[LAST_ERROR] = f"{self._last_error.description} ({self._last_error.code})"
+ attributes[
+ LAST_ERROR
+ ] = f"{self._last_error.description} ({self._last_error.code})"
return attributes
@property
- def device_info(self) -> Optional[Dict[str, Any]]:
+ def device_info(self) -> DeviceInfo:
+ """Return device specific attributes."""
return get_device_info(self._device)
- async def async_set_fan_speed(self, fan_speed: str, **kwargs):
+ async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
+ """Set fan speed."""
await self._device.execute_command(SetFanSpeed(fan_speed))
- async def async_return_to_base(self, **kwargs):
+ async def async_return_to_base(self, **kwargs: Any) -> None:
"""Set the vacuum cleaner to return to the dock."""
await self._device.execute_command(Charge())
- async def async_stop(self, **kwargs):
+ async def async_stop(self, **kwargs: Any) -> None:
"""Stop the vacuum cleaner."""
await self._device.execute_command(CleanStop())
- async def async_pause(self):
+ async def async_pause(self) -> None:
"""Pause the vacuum cleaner."""
await self._device.execute_command(CleanPause())
- async def async_start(self):
+ async def async_start(self) -> None:
"""Start the vacuum cleaner."""
if self._device.status.state == VacuumState.STATE_PAUSED:
await self._device.execute_command(CleanResume())
else:
await self._device.execute_command(CleanStart())
- async def async_locate(self, **kwargs):
+ async def async_locate(self, **kwargs: Any) -> None:
"""Locate the vacuum cleaner."""
await self._device.execute_command(PlaySound())
- async def async_send_command(self, command, params=None, **kwargs):
+ async def async_send_command(
+ self, command: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any
+ ) -> None:
"""Send a command to a vacuum cleaner."""
- _LOGGER.debug(f"async_send_command {command} with {params}")
-
- if command == "spot_area":
- await self._device.execute_command(
- CleanSpotArea(area=str(params["rooms"]), cleanings=params.get("cleanings", 1)))
- elif command == "custom_area":
- await self._device.execute_command(CleanCustomArea(
- map_position=str(params["coordinates"]), cleanings=params.get("cleanings", 1)))
- elif command == "set_water":
- await self._device.execute_command(SetWaterLevel(params["amount"]))
- elif command == "relocate":
+ _LOGGER.debug("async_send_command %s with %s", command, params)
+
+ if command == "relocate":
await self._device.execute_command(Relocate())
elif command == "auto_clean":
- await self._device.execute_command(CleanStart(params.get("type", "auto")))
+ clean_type = params.get("type", "auto") if params else "auto"
+ await self._device.execute_command(CleanStart(clean_type))
+ elif command in ["spot_area", "custom_area", "set_water"]:
+ if params is None:
+ raise RuntimeError("Params are required!")
+
+ if command == "spot_area":
+ await self._device.execute_command(
+ CleanSpotArea(
+ area=str(params["rooms"]), cleanings=params.get("cleanings", 1)
+ )
+ )
+ elif command == "custom_area":
+ await self._device.execute_command(
+ CleanCustomArea(
+ map_position=str(params["coordinates"]),
+ cleanings=params.get("cleanings", 1),
+ )
+ )
+ elif command == "set_water":
+ await self._device.execute_command(SetWaterLevel(params["amount"]))
else:
await self._device.execute_command(Command(command, params))
async def _service_refresh(self, part: str) -> None:
- """Service to manually refresh"""
- _LOGGER.debug(f"Manually refresh {part}")
+ """Service to manually refresh."""
+ _LOGGER.debug("Manually refresh %s", part)
if part == EVENT_STATUS:
- self._device.statusEvents.request_refresh()
+ self._device.events.status.request_refresh()
elif part == EVENT_ERROR:
- self._device.errorEvents.request_refresh()
+ self._device.events.error.request_refresh()
elif part == EVENT_FAN_SPEED:
- self._device.fanSpeedEvents.request_refresh()
+ self._device.events.fan_speed.request_refresh()
elif part == EVENT_CLEAN_LOGS:
- self._device.cleanLogsEvents.request_refresh()
+ self._device.events.clean_logs.request_refresh()
elif part == EVENT_WATER:
- self._device.waterEvents.request_refresh()
+ self._device.events.water_info.request_refresh()
elif part == EVENT_BATTERY:
- self._device.batteryEvents.request_refresh()
+ self._device.events.battery.request_refresh()
elif part == EVENT_STATS:
- self._device.statsEvents.request_refresh()
+ self._device.events.stats.request_refresh()
elif part == EVENT_LIFE_SPAN:
- self._device.lifespanEvents.request_refresh()
+ self._device.events.lifespan.request_refresh()
elif part == EVENT_ROOMS:
- self._device.map.roomsEvents.request_refresh()
+ self._device.events.rooms.request_refresh()
elif part == EVENT_MAP:
- self._device.map.mapEvents.request_refresh()
+ self._device.events.map.request_refresh()
else:
- _LOGGER.warning(f"Service \"refresh\" called with unknown part: {part}")
+ _LOGGER.warning('Service "refresh" called with unknown part: %s', part)
diff --git a/docs/examples/room-queue-nodered.md b/docs/examples/room-queue-nodered.md
index e101ef8..a8b2e2e 100644
--- a/docs/examples/room-queue-nodered.md
+++ b/docs/examples/room-queue-nodered.md
@@ -1,35 +1,39 @@
# Room Queuing (NodeRED)
-This NodeRED Flow creates a cleaning queue.
-
-Main advantage is the capability to add rooms on the fly without interrupting current jobs. Selecting a new room won't cancel the current cleaning job. Ideal for creating cleaning jobs by physical scene switches (like Aqara Opple) or conditions (like no motion in the room). Cleaning will be done first-in-first-out. The max lenght of the queue is 10 for a little girlfriend/wife/child safety.
+This NodeRED Flow creates a cleaning queue.
+Main advantage is the capability to add rooms on the fly without interrupting current jobs. Selecting a new room won't cancel the current cleaning job. Ideal for creating cleaning jobs by physical scene switches (like Aqara Opple) or conditions (like no motion in the room). Cleaning will be done first-in-first-out. The max length of the queue is 10 for a little girlfriend/wife/child safety.
![Preview](../images/room-queue-nodered_flow.png)
Adding a room to the queue can be done by triggering the event `walleroom` with the following data. Injecting the data by replacing the event-node is possible, too.
+
```yaml
type: room
-room: '{{ states.vacuum.wall_e.attributes.room_study}}'
+room: "{{ states.vacuum.wall_e.attributes.room_study}}"
count: 1
fan: normal
-```
-event data | meaning
------------- | -------------
-type | _room_ (for rooms) OR _zone_ (for custom zones using coordinates)
-room | depending on the type ↑
room: room number (numerical or dynamic as mentioned in the Readme below Templates)
zone: coordinates (e.g. _'-1339,-1511,296,-2587'_)
-count | _1_ or _2_ (cleaning quantity)
-fan | fan speed in plain text
+```
+
+| event data | meaning |
+| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| type | _room_ (for rooms) OR _zone_ (for custom zones using coordinates) |
+| room | depending on the type ↑
room: room number (numerical or dynamic as mentioned in the Readme below Templates)
zone: coordinates (e.g. _'-1339,-1511,296,-2587'_) |
+| count | _1_ or _2_ (cleaning quantity) |
+| fan | fan speed in plain text |
Returning the robovac via `vacuum.return_to_base` or restarting NodeRED empties the queue.
## Name of the robovac
+
The robovac is called Wall-E in this example. To change the name, you need to change the entity id in all Home Assistant nodes (the blue ones) except the `Call Service` node. Renaming the event triggering an addition can be done in the `events: walleroom` node
## Issues
+
A new cleaning job is created every room. So the `last_clean_image` does only show the picture of the latest room.
## NodeRED Flow
+
```
[{"id":"e322f35b.77d14","type":"function","z":"4a944fd0.0aea7","name":"Queue catch","func":"if (msg.queue[0] == null) return [null, msg];\nelse {\nmsg.nextroom= msg.queue[0];\nfor (i=0; msg.queue[i] != null ;i++){ \nmsg.queue[i]= msg.queue[i+1];\n}\nreturn [msg, null];\n}","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":300,"wires":[["944a9632.030ec8"],[]]},{"id":"6b764e8e.a9e6f","type":"server-events","z":"4a944fd0.0aea7","name":"","server":"62bd7c3c.e8e914","version":1,"event_type":"walleroom","exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"waitForRunning":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"}],"x":130,"y":220,"wires":[["6422bfc5.8fda3"]]},{"id":"73d11df7.97f704","type":"function","z":"4a944fd0.0aea7","name":"Queue add","func":"if (msg.queue[0] == null) {\n msg.queue[0]= msg.payload.event\n return [msg];\n}\n\nfor (i=9; msg.queue[i] == null ;i--){ \n if (msg.queue[i-1] != null){\nmsg.queue[i]= msg.payload.event\nreturn [msg];\n }}\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":220,"wires":[["8ccea2f9.c117a"]]},{"id":"cd3abbab.6e8e78","type":"change","z":"4a944fd0.0aea7","name":"delete queue","rules":[{"t":"set","p":"queue","pt":"flow","to":"[null,null,null,null,null,null,null,null,null,null]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":450,"y":100,"wires":[[]]},{"id":"9e90e841.5791c8","type":"inject","z":"4a944fd0.0aea7","name":"Restart","props":[{"p":"queue","v":"[null,null,null,null,null,null,null,null,null,null]","vt":"json"}],"repeat":"","crontab":"","once":true,"onceDelay":"5","topic":"","x":280,"y":100,"wires":[["cd3abbab.6e8e78"]]},{"id":"12119484.65cb9b","type":"change","z":"4a944fd0.0aea7","name":"","rules":[{"t":"set","p":"queue","pt":"msg","to":"queue","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":300,"wires":[["e322f35b.77d14"]]},{"id":"944a9632.030ec8","type":"change","z":"4a944fd0.0aea7","name":"","rules":[{"t":"set","p":"queue","pt":"flow","to":"queue","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":280,"wires":[["648a462a.f05078"]]},{"id":"6422bfc5.8fda3","type":"change","z":"4a944fd0.0aea7","name":"","rules":[{"t":"set","p":"queue","pt":"msg","to":"queue","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":220,"wires":[["73d11df7.97f704"]]},{"id":"8ccea2f9.c117a","type":"change","z":"4a944fd0.0aea7","name":"","rules":[{"t":"set","p":"queue","pt":"flow","to":"queue","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":220,"wires":[["b92ee95a.a9bcc8","f7256e02.94227"]]},{"id":"cbd0ea46.ad3568","type":"server-events","z":"4a944fd0.0aea7","name":"Call Service","server":"62bd7c3c.e8e914","version":1,"event_type":"call_service","exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"waitForRunning":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"}],"x":110,"y":140,"wires":[["3b4c86f9.6a8f6a"]]},{"id":"3b4c86f9.6a8f6a","type":"switch","z":"4a944fd0.0aea7","name":"Return to base?","property":"payload.event.service","propertyType":"msg","rules":[{"t":"eq","v":"return_to_base","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":340,"y":140,"wires":[["cd3abbab.6e8e78"]]},{"id":"4aec9575.96167c","type":"server-state-changed","z":"4a944fd0.0aea7","name":"Walle docking","server":"62bd7c3c.e8e914","version":3,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"vacuum.wall_e","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"returning","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"for":"2","forType":"num","forUnits":"seconds","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":110,"y":300,"wires":[["12119484.65cb9b"],[]]},{"id":"648a462a.f05078","type":"api-call-service","z":"4a944fd0.0aea7","name":"Wall-E FanSpeed","server":"62bd7c3c.e8e914","version":3,"debugenabled":false,"service_domain":"vacuum","service":"set_fan_speed","entityId":"vacuum.wall_e","data":"{\"fan_speed\":\"{{nextroom.fan}}\"}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":930,"y":280,"wires":[["af6f30aa.0f7d"]]},{"id":"9f3a7d63.1a37a","type":"switch","z":"4a944fd0.0aea7","name":"Raum/Zone?","property":"msg.nextroom.type","propertyType":"msg","rules":[{"t":"eq","v":"room","vt":"str"},{"t":"eq","v":"zone","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":1470,"y":280,"wires":[["e33af8b2.3c7b98"],["155a4e4b.08dcb2"]]},{"id":"af6f30aa.0f7d","type":"api-call-service","z":"4a944fd0.0aea7","name":"Wall-E CleanCounts","server":"62bd7c3c.e8e914","version":3,"debugenabled":false,"service_domain":"vacuum","service":"send_command","entityId":"vacuum.wall_e","data":"{ \"command\": \"setCleanCount\", \"params\": { \"count\": {{nextroom.count}} }}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1140,"y":280,"wires":[["1904cc73.0c0084"]]},{"id":"155a4e4b.08dcb2","type":"api-call-service","z":"4a944fd0.0aea7","name":"Wall-E Go","server":"62bd7c3c.e8e914","version":3,"debugenabled":false,"service_domain":"vacuum","service":"send_command","entityId":"vacuum.wall_e","data":"{\"command\":\"custom_area\",\"params\":{\"coordinates\":\"{{nextroom.room}}\"}}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1620,"y":320,"wires":[[]]},{"id":"e33af8b2.3c7b98","type":"api-call-service","z":"4a944fd0.0aea7","name":"Wall-E Go","server":"62bd7c3c.e8e914","version":3,"debugenabled":true,"service_domain":"vacuum","service":"send_command","entityId":"vacuum.wall_e","data":"{\"command\":\"spot_area\",\"params\":{\"rooms\":\"{{nextroom.room}}\"}}","dataType":"json","mergecontext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1640,"y":260,"wires":[[]]},{"id":"f7256e02.94227","type":"api-current-state","z":"4a944fd0.0aea7","name":"Docked?","server":"62bd7c3c.e8e914","version":2,"outputs":2,"halt_if":"docked","halt_if_type":"str","halt_if_compare":"is","entity_id":"vacuum.wall_e","state_type":"str","blockInputOverrides":false,"outputProperties":[],"override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":900,"y":220,"wires":[["12119484.65cb9b"],[]]},{"id":"b92ee95a.a9bcc8","type":"api-current-state","z":"4a944fd0.0aea7","name":"Returning","server":"62bd7c3c.e8e914","version":2,"outputs":2,"halt_if":"returning","halt_if_type":"str","halt_if_compare":"is","entity_id":"vacuum.wall_e","state_type":"str","blockInputOverrides":false,"outputProperties":[],"override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":900,"y":180,"wires":[["12119484.65cb9b"],[]]},{"id":"9938b7ae.ccf6f8","type":"server-state-changed","z":"4a944fd0.0aea7","name":"Walle docked","server":"62bd7c3c.e8e914","version":3,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"vacuum.wall_e","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"docked","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"for":"2","forType":"num","forUnits":"seconds","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"x":110,"y":340,"wires":[["12119484.65cb9b"],[]]},{"id":"1904cc73.0c0084","type":"delay","z":"4a944fd0.0aea7","name":"2s","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1310,"y":280,"wires":[["9f3a7d63.1a37a"]]},{"id":"9dc0863c.5bbb68","type":"comment","z":"4a944fd0.0aea7","name":"Wall-E Queue","info":"","x":720,"y":160,"wires":[]},{"id":"62bd7c3c.e8e914","type":"server","name":"Home Assistant","version":1,"legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":false,"cacheJson":true}]
```
diff --git a/docs/examples/ui-advanced.md b/docs/examples/ui-advanced.md
index 213a56f..669e45f 100644
--- a/docs/examples/ui-advanced.md
+++ b/docs/examples/ui-advanced.md
@@ -1,13 +1,14 @@
# UI advanced example
Main feature is ability to specify via UI the order of the cleaned rooms.
-Thanks to @aidbish for the inital idea.
+Thanks to @aidbish for the initial idea.
![Preview](../images/ui-advanced.gif)
## Setup
For this setup to work, you need some custom components (all available via HACS)
+
- [Variables](https://github.com/Wibias/hass-variables)
- [Vacuum-card](https://github.com/denysdovhan/vacuum-card)
- [Button-card](https://github.com/custom-cards/button-card)
@@ -107,7 +108,6 @@ variable:
value: ""
restore: false
-
# Room name comes from the integration to match attribute names
template:
unique_id: deebot_susi_queue
@@ -168,13 +168,13 @@ cards:
stats:
default:
- entity_id: sensor.susi_brush
- unit: '%'
+ unit: "%"
subtitle: Hauptbürste
- entity_id: sensor.susi_sidebrush
- unit: '%'
+ unit: "%"
subtitle: Seitenbürsten
- entity_id: sensor.susi_heap
- unit: '%'
+ unit: "%"
subtitle: Filter
cleaning:
- entity_id: sensor.susi_stats_area
@@ -206,7 +206,7 @@ cards:
- styles:
card:
- background-color: var(--primary-color)
- operator: '>='
+ operator: ">="
value: 1
styles:
card:
@@ -246,7 +246,7 @@ cards:
- styles:
card:
- background-color: var(--primary-color)
- operator: '>='
+ operator: ">="
value: 1
styles:
card:
@@ -286,7 +286,7 @@ cards:
- styles:
card:
- background-color: var(--primary-color)
- operator: '>='
+ operator: ">="
value: 1
styles:
card:
@@ -326,7 +326,7 @@ cards:
- styles:
card:
- background-color: var(--primary-color)
- operator: '>='
+ operator: ">="
value: 1
styles:
card:
@@ -368,7 +368,7 @@ cards:
- styles:
card:
- background-color: var(--primary-color)
- operator: '>='
+ operator: ">="
value: 1
styles:
card:
@@ -408,7 +408,7 @@ cards:
- styles:
card:
- background-color: var(--primary-color)
- operator: '>='
+ operator: ">="
value: 1
styles:
card:
@@ -448,7 +448,7 @@ cards:
- styles:
card:
- background-color: var(--primary-color)
- operator: '>='
+ operator: ">="
value: 1
styles:
card:
@@ -552,4 +552,4 @@ cards:
}
```
-If someone finds an easier solution to configure the same, please feel free to adopt this example.
\ No newline at end of file
+If someone finds an easier solution to configure the same, please feel free to adopt this example.
diff --git a/docs/examples/ui-simple.md b/docs/examples/ui-simple.md
index 82a768a..562e137 100644
--- a/docs/examples/ui-simple.md
+++ b/docs/examples/ui-simple.md
@@ -5,7 +5,7 @@ A suggested custom lovelace card that i use is: vacuum-card by denysdovhan link:
## Configuration:
```yaml
-type: 'custom:vacuum-card'
+type: "custom:vacuum-card"
entity: vacuum.YOURROBOTNAME
image: default
compact_view: false
@@ -15,13 +15,13 @@ show_status: true
stats:
default:
- entity_id: sensor.YOURROBOTNAME_sidebrush
- unit: '%'
+ unit: "%"
subtitle: Side Brush
- entity_id: sensor.YOURROBOTNAME_brush
- unit: '%'
+ unit: "%"
subtitle: Main Brush
- entity_id: sensor.YOURROBOTNAME_heap
- unit: '%'
+ unit: "%"
subtitle: Heap
cleaning:
- entity_id: sensor.YOURROBOTNAME_stats_area
@@ -32,14 +32,14 @@ stats:
subtitle: Time
actions:
- service: script.CLEAN_LIVINGROOM
- icon: 'mdi:sofa'
+ icon: "mdi:sofa"
- service: script.CLEAN_BEDROOM
- icon: 'mdi:bed-empty'
+ icon: "mdi:bed-empty"
- service: script.CLEAN_ALL
- icon: 'mdi:robot-vacuum-variant'
+ icon: "mdi:robot-vacuum-variant"
map: camera.ROBOTNAME_liveMap
```
Something like this should be the result:
-![Preview](../images/custom_vacuum_card.jpg)
\ No newline at end of file
+![Preview](../images/custom_vacuum_card.jpg)
diff --git a/info.md b/info.md
index 5cf83a7..1da6cc7 100644
--- a/info.md
+++ b/info.md
@@ -39,7 +39,7 @@ To add your Ecovacs devices into your Home Assistant installation, follow the st
### 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.
+For chinese server username you require "short id" and password. short id look like "EXXXXXX". DO NOT USE YOUR MOBILE PHONE NUMBER, it won't work.
country: cn
continent: as (or ww)
@@ -100,7 +100,7 @@ Example for fan_speed:
{{ states.vacuum.YOUR_ROBOT_NAME.attributes['fan_speed'] }}
```
-Get room numbers dynamically, very helpfull if your robot is multi-floor or if your robot lose the map and you don't want to change automations every time:
+Get room numbers dynamically, very helpful if your robot is multi-floor or if your robot lose the map and you don't want to change automations every time:
```
{{ states.vacuum.YOURROBOTNAME.attributes.room_bathroom }}
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..b60f014
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,19 @@
+[mypy]
+python_version = 3.8
+show_error_codes = true
+follow_imports = silent
+ignore_missing_imports = true
+strict_equality = true
+warn_incomplete_stub = true
+warn_redundant_casts = true
+warn_unused_configs = true
+warn_unused_ignores = true
+check_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
+disallow_untyped_calls = true
+disallow_untyped_decorators = true
+disallow_untyped_defs = true
+no_implicit_optional = true
+warn_return_any = true
+warn_unreachable = true
\ No newline at end of file
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 0000000..8e73d35
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,49 @@
+[MASTER]
+ignore=tests
+# Use a conservative default here; 2 should speed up most setups and not hurt
+# any too bad. Override on command line as appropriate.
+# Disabled for now: https://github.com/PyCQA/pylint/issues/3584
+#jobs=2
+load-plugins=pylint_strict_informational
+persistent=no
+extension-pkg-whitelist=ciso8601,cv2
+
+[BASIC]
+good-names=i,j,k,ex,_,T,x,y,id
+
+[MESSAGES CONTROL]
+# Reasons disabled:
+# format - handled by black
+# duplicate-code - unavoidable
+# cyclic-import - doesn't test if both import on load
+# too-many-* - are not enforced for the sake of readability
+# abstract-method - with intro of async there are always methods missing
+# inconsistent-return-statements - doesn't handle raise
+# wrong-import-order - isort guards this
+disable=
+ format,
+ abstract-class-little-used,
+ abstract-method,
+ cyclic-import,
+ duplicate-code,
+ inconsistent-return-statements,
+ too-many-instance-attributes,
+ wrong-import-order,
+ too-few-public-methods
+
+# enable useless-suppression temporarily every now and then to clean them up
+enable=
+ use-symbolic-message-instead,
+
+[REPORTS]
+score=no
+
+[TYPECHECK]
+# For attrs
+ignored-classes=_CountingAttr
+
+[FORMAT]
+expected-line-ending-format=LF
+
+[EXCEPTIONS]
+overgeneral-exceptions=BaseException,Exception
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f11cc9d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,12 @@
+deebotozmo==3.0.0b1
+homeassistant==2021.9.7
+
+# Test requirements
+#pytest-homeassistant-custom-component==0.4.4
+black==21.9b0
+flake8==3.9.2
+isort==5.9.3
+mypy==0.910
+pylint==2.11.1
+pylint-strict-informational==0.1
+pre-commit==2.15.0
diff --git a/requirements_dev.txt b/requirements_dev.txt
deleted file mode 100644
index 7d78f01..0000000
--- a/requirements_dev.txt
+++ /dev/null
@@ -1 +0,0 @@
-homeassistant
diff --git a/requirements_test.txt b/requirements_test.txt
deleted file mode 100644
index ee5e049..0000000
--- a/requirements_test.txt
+++ /dev/null
@@ -1 +0,0 @@
-pytest-homeassistant-custom-component==0.4.0
diff --git a/scripts/run-in-env.sh b/scripts/run-in-env.sh
new file mode 100755
index 0000000..0f531f2
--- /dev/null
+++ b/scripts/run-in-env.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env sh
+set -eu
+
+# Activate pyenv and virtualenv if present, then run the specified command
+
+# pyenv, pyenv-virtualenv
+if [ -s .python-version ]; then
+ PYENV_VERSION=$(head -n 1 .python-version)
+ export PYENV_VERSION
+fi
+
+# other common virtualenvs
+my_path=$(git rev-parse --show-toplevel)
+
+for venv in venv .venv .; do
+ if [ -f "${my_path}/${venv}/bin/activate" ]; then
+ . "${my_path}/${venv}/bin/activate"
+ fi
+done
+
+exec "$@"
diff --git a/setup.cfg b/setup.cfg
index 4ee3655..0df93d1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -7,29 +7,15 @@ max-line-length = 88
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
-# W504 line break after binary operator
+# D107 Missing docstring in __init__
ignore =
E501,
W503,
E203,
D202,
- W504
+ D107
[isort]
# https://github.com/timothycrosley/isort
# https://github.com/timothycrosley/isort/wiki/isort-Settings
-# splits long import on multiple lines indented by 4 spaces
-multi_line_output = 3
-include_trailing_comma=True
-force_grid_wrap=0
-use_parentheses=True
-line_length=88
-indent = " "
-# by default isort don't check module indexes
-not_skip = __init__.py
-# will group `import x` and `from x import` of the same module.
-force_sort_within_sections = true
-sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
-default_section = THIRDPARTY
-known_first_party = custom_components.integration_blueprint, tests
-combine_as_imports = true
+profile = black
\ No newline at end of file
diff --git a/tests/README.md b/tests/README.md
index 90017d4..3913777 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -5,6 +5,7 @@ While tests aren't required to publish a custom component for Home Assistant, th
# Getting Started
To begin, it is recommended to create a virtual environment to install dependencies:
+
```bash
python3 -m venv venv
source venv/bin/activate
@@ -17,8 +18,8 @@ This will install `homeassistant`, `pytest`, and `pytest-homeassistant-custom-co
# Useful commands
-Command | Description
-------- | -----------
-`pytest tests/` | This will run all tests in `tests/` and tell you how many passed/failed
-`pytest --durations=10 --cov-report term-missing --cov=custom_components.integration_blueprint tests` | This tells `pytest` that your target module to test is `custom_components.integration_blueprint` so that it can give you a [code coverage](https://en.wikipedia.org/wiki/Code_coverage) summary, including % of code that was executed and the line numbers of missed executions.
-`pytest tests/test_init.py -k test_setup_unload_and_reload_entry` | Runs the `test_setup_unload_and_reload_entry` test function located in `tests/test_init.py`
+| Command | Description |
+| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `pytest tests/` | This will run all tests in `tests/` and tell you how many passed/failed |
+| `pytest --durations=10 --cov-report term-missing --cov=custom_components.integration_blueprint tests` | This tells `pytest` that your target module to test is `custom_components.integration_blueprint` so that it can give you a [code coverage](https://en.wikipedia.org/wiki/Code_coverage) summary, including % of code that was executed and the line numbers of missed executions. |
+| `pytest tests/test_init.py -k test_setup_unload_and_reload_entry` | Runs the `test_setup_unload_and_reload_entry` test function located in `tests/test_init.py` |