├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── translation.yml │ ├── feature.yml │ └── issue.yml ├── FUNDING.yml ├── workflows │ ├── debug.yml │ ├── release-drafter.yml │ ├── validation.yml │ ├── format-code.yml │ └── release.yml ├── dependabot.yml ├── release-drafter.yml └── scripts │ └── update_hacs_manifest.py ├── custom_components └── landroid_cloud │ ├── utils │ ├── __init__.py │ ├── entity_setup.py │ ├── platform_setup.py │ ├── schedules.py │ └── logger.py │ ├── attribute_map.py │ ├── devices │ ├── __init__.py │ ├── kress.py │ ├── ferrex.py │ ├── landxcape.py │ └── worx.py │ ├── icons.json │ ├── manifest.json │ ├── lawn_mower.py │ ├── translations │ ├── nb.json │ ├── no.json │ ├── ru.json │ ├── fr.json │ ├── ro_RO.json │ ├── de.json │ ├── sv.json │ ├── hu.json │ ├── cs.json │ └── et.json │ ├── select.py │ ├── diagnostics.py │ ├── binary_sensor.py │ ├── device_action.py │ ├── scheme.py │ ├── device_condition.py │ ├── button.py │ ├── device_trigger.py │ ├── switch.py │ ├── number.py │ ├── services.py │ ├── config_flow.py │ ├── services.yaml │ ├── api.py │ ├── __init__.py │ ├── sensor.py │ └── const.py ├── requirements.txt ├── scripts ├── lint ├── setup └── develop ├── hacs.json ├── .gitignore ├── .vscode ├── tasks.json └── settings.json ├── config └── configuration.yaml ├── .devcontainer.json ├── .ruff.toml └── README.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.buymeacoffee.com/mtrab 2 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Landroid Cloud utilities.""" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorlog==6.10.1 2 | homeassistant==2025.12.0 3 | pip>=21.0,<25.4 4 | ruff==0.14.7 5 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | ruff check . --fix -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Landroid Cloud", 3 | "render_readme": false, 4 | "homeassistant": "2024.10.0", 5 | "zip_release": true, 6 | "filename": "landroid_cloud.zip" 7 | } -------------------------------------------------------------------------------- /.github/workflows/debug.yml: -------------------------------------------------------------------------------- 1 | name: Debug GIT environment 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: hmarr/debug-action@v3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # artifacts 2 | __pycache__ 3 | .pytest* 4 | *.egg-info 5 | */build/* 6 | */dist/* 7 | 8 | 9 | # misc 10 | .coverage 11 | .vscode 12 | coverage.xml 13 | 14 | 15 | # Home Assistant configuration 16 | config/* 17 | !config/configuration.yaml -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run Home Assistant developer env on port 9123", 6 | "type": "shell", 7 | "command": "scripts/develop", 8 | "problemMatcher": [] 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/attribute_map.py: -------------------------------------------------------------------------------- 1 | """Attribute map used by Landroid Cloud integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from .const import ATTR_ACCESSORIES, ATTR_LAWN 6 | 7 | ATTR_MAP = { 8 | "accessories": ATTR_ACCESSORIES, 9 | "lawn": ATTR_LAWN, 10 | } 11 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_amd64 --output /bin/go2rtc 6 | chmod a+x /bin/go2rtc 7 | pip3 install uv==0.6.8 8 | 9 | cd "$(dirname "$0")/.." 10 | 11 | python3 -m pip install --requirement requirements.txt -------------------------------------------------------------------------------- /config/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | 3 | automation: !include automations.yaml 4 | 5 | logger: 6 | default: warning 7 | logs: 8 | custom_components.landroid_cloud: debug 9 | pyworxcloud: debug 10 | # If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) 11 | # debugpy: 12 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/devices/__init__.py: -------------------------------------------------------------------------------- 1 | """Landroid Cloud device definitions.""" 2 | 3 | from __future__ import annotations 4 | 5 | from . import ferrex as Ferrex 6 | from . import kress as Kress 7 | from . import landxcape as LandXcape 8 | from . import worx as Worx 9 | 10 | __all__ = ["Worx", "Kress", "LandXcape", "Ferrex"] # Devices to export 11 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "restart": "mdi:restart", 4 | "edgecut": "mdi:robot-mower", 5 | "setzone": "mdi:map-clock", 6 | "config": "mdi:cog", 7 | "ots": "mdi:robot-mower", 8 | "schedule": "mdi:calendar-clock", 9 | "torque": "mdi:arm-flex", 10 | "send_raw": "mdi:send-circle" 11 | } 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.yaml": "home-assistant" 4 | }, 5 | "[python]": { 6 | "editor.defaultFormatter": "ms-python.black-formatter", 7 | "editor.formatOnSave": true, 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": "explicit" 10 | }, 11 | }, 12 | "isort.args": [ 13 | "--profile", 14 | "black" 15 | ], 16 | "python.analysis.autoImportCompletions": true 17 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "landroid_cloud", 3 | "name": "Landroid Cloud", 4 | "after_dependencies": [ 5 | "http" 6 | ], 7 | "codeowners": [ 8 | "@MTrab" 9 | ], 10 | "config_flow": true, 11 | "documentation": "https://github.com/MTrab/landroid_cloud/blob/master/README.md", 12 | "iot_class": "cloud_push", 13 | "issue_tracker": "https://github.com/MTrab/landroid_cloud/issues", 14 | "loggers": [ 15 | "pyworxcloud" 16 | ], 17 | "requirements": [ 18 | "pyworxcloud==5.0.0" 19 | ], 20 | "version": "6.0.0" 21 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/utils/entity_setup.py: -------------------------------------------------------------------------------- 1 | """Utilities used for entity setup.""" 2 | 3 | # pylint: disable=relative-beyond-top-level 4 | from __future__ import annotations 5 | 6 | from ..device_base import LandroidCloudMowerBase 7 | from ..devices import Kress, LandXcape, Worx 8 | 9 | 10 | def vendor_to_device(vendor: str): 11 | """Map vendor to device class.""" 12 | device: LandroidCloudMowerBase = None 13 | if vendor == "worx": 14 | device = Worx 15 | elif vendor == "kress": 16 | device = Kress 17 | elif vendor == "landxcape": 18 | device = LandXcape 19 | return device 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | labels: 8 | - dependencies 9 | - patch 10 | - skip-changelog 11 | - package-ecosystem: pip 12 | directory: "/.github/workflows" 13 | schedule: 14 | interval: daily 15 | labels: 16 | - dependencies 17 | - patch 18 | - skip-changelog 19 | - package-ecosystem: pip 20 | directory: "/" 21 | schedule: 22 | interval: daily 23 | time: "04:00" 24 | labels: 25 | - dependencies 26 | - patch 27 | - skip-changelog 28 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | types: [opened, reopened, synchronize] 10 | 11 | jobs: 12 | update_release_draft: 13 | name: Update release draft 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v6 18 | with: 19 | fetch-depth: 0 20 | - name: Create Release 21 | uses: release-drafter/release-drafter@v6 22 | with: 23 | disable-releaser: github.ref != 'refs/heads/main' 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/translation.yml: -------------------------------------------------------------------------------- 1 | name: Language request 2 | description: Make a request for a new language added to the Lokalise project. 3 | title: "[LR]: " 4 | labels: ["translation"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | This form is only for requesting a new language added for translation in Lokalise. 10 | 11 | Remember to add the requested language in the title. 12 | - type: textarea 13 | validations: 14 | required: true 15 | attributes: 16 | label: Requested language 17 | - type: textarea 18 | validations: 19 | required: true 20 | attributes: 21 | label: Language code (ie. de for German) 22 | -------------------------------------------------------------------------------- /.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Code validation 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | validate-hassfest: 10 | name: Hassfest validation 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v6 15 | - name: validation 16 | uses: home-assistant/actions/hassfest@master 17 | 18 | validate-hacs: 19 | name: HACS validation 20 | runs-on: "ubuntu-latest" 21 | steps: 22 | - name: checkout 23 | uses: "actions/checkout@v6" 24 | - name: validation 25 | uses: "hacs/action@main" 26 | with: 27 | category: "integration" 28 | -------------------------------------------------------------------------------- /scripts/develop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | #Force removal of pyworxcloud to ensure reinstall every time 8 | pip uninstall -y pyworxcloud 9 | 10 | # Create config dir if not present 11 | if [[ ! -d "${PWD}/config" ]]; then 12 | mkdir -p "${PWD}/config" 13 | hass --config "${PWD}/config" --script ensure_config 14 | fi 15 | 16 | # Set the path to custom_components 17 | ## This let's us have the structure we want /custom_components/integration_blueprint 18 | ## while at the same time have Home Assistant configuration inside /config 19 | ## without resulting to symlinks. 20 | export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" 21 | 22 | # Start Home Assistant 23 | hass --config "${PWD}/config" --debug -------------------------------------------------------------------------------- /custom_components/landroid_cloud/utils/platform_setup.py: -------------------------------------------------------------------------------- 1 | """Setup entity platforms.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.const import __version__ as ha_version 9 | from homeassistant.core import HomeAssistant 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | async def async_setup_entity_platforms( 15 | hass: HomeAssistant, 16 | config_entry: ConfigEntry, 17 | platforms: list[str], 18 | ) -> None: 19 | """Set up entity platforms.""" 20 | 21 | if ha_version >= "2022.8.0.dev0": 22 | _LOGGER.debug("Using post 2022.8 style loading.") 23 | await hass.config_entries.async_forward_entry_setups(config_entry, platforms) 24 | else: 25 | _LOGGER.debug("Using pre 2022.8 style loading.") 26 | hass.config_entries.async_setup_platforms(config_entry, platforms) 27 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/lawn_mower.py: -------------------------------------------------------------------------------- 1 | """Support for Landroid cloud compatible mowers.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import HomeAssistant 7 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 8 | 9 | from .api import LandroidAPI 10 | from .const import ATTR_DEVICES, DOMAIN 11 | from .utils.entity_setup import vendor_to_device 12 | 13 | 14 | async def async_setup_entry( 15 | hass: HomeAssistant, 16 | config: ConfigEntry, 17 | async_add_entities: AddEntitiesCallback, 18 | ) -> None: 19 | """Set up the mower device.""" 20 | mowers = [] 21 | for _name, info in hass.data[DOMAIN][config.entry_id][ATTR_DEVICES].items(): 22 | api: LandroidAPI = info["api"] 23 | device = vendor_to_device(api.config["type"]) 24 | constructor = device.MowerDevice(hass, api) 25 | 26 | mowers.append(constructor) 27 | 28 | async_add_entities(mowers, True) 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Make a request for a new feature. 3 | title: "[FR]: " 4 | labels: ["feature request"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | This form is only for requesting new features to the integration. 10 | 11 | Remember to add a descriptive title after the predefined text. 12 | - type: textarea 13 | validations: 14 | required: true 15 | attributes: 16 | label: Describe the feature you wish to make a request for 17 | description: >- 18 | Provide a clear and concise description of what you wish for. 19 | The more precise and detailed the more likely it is to be accepted and made. 20 | - type: textarea 21 | attributes: 22 | label: Describe alternatives you've considered 23 | description: >- 24 | Have you considered any alternatives (ie. other existing integrations that can handle this) 25 | - type: textarea 26 | attributes: 27 | label: Additional context 28 | description: >- 29 | Add any other context or screenshots about the feature request here. 30 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Landroid Cloud", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.13-bookworm", 4 | "postCreateCommand": "scripts/setup", 5 | "appPort": [ 6 | "9123:8123" 7 | ], 8 | "containerEnv": { "TZ": "Europe/Copenhagen" }, 9 | "portsAttributes": { 10 | "8123": { 11 | "label": "Home Assistant - Landroid Cloud", 12 | "onAutoForward": "notify" 13 | } 14 | }, 15 | "customizations": { 16 | "vscode": { 17 | "extensions": [ 18 | "ms-python.python", 19 | "github.vscode-pull-request-github", 20 | "ryanluker.vscode-coverage-gutters", 21 | "ms-python.vscode-pylance", 22 | "ms-python.isort", 23 | "ms-python.black-formatter" 24 | ], 25 | "settings": { 26 | "files.eol": "\n", 27 | "editor.tabSize": 4, 28 | "terminal.integrated.shell.linux": "/bin/bash", 29 | "python.defaultInterpreterPath": "/usr/bin/python3", 30 | "python.analysis.autoSearchPaths": false, 31 | "python.linting.pylintEnabled": true, 32 | "python.linting.enabled": true, 33 | "python.formatting.provider": "black", 34 | "editor.formatOnPaste": false, 35 | "editor.formatOnSave": true, 36 | "editor.formatOnType": true, 37 | "files.trimTrailingWhitespace": true 38 | } 39 | } 40 | }, 41 | "remoteUser": "root", 42 | "features": { 43 | "ghcr.io/devcontainers/features/rust:1": {} 44 | } 45 | } -------------------------------------------------------------------------------- /.github/workflows/format-code.yml: -------------------------------------------------------------------------------- 1 | name: Format code 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | format: 11 | name: Format with black and isort 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v6 16 | with: 17 | fetch-depth: 0 18 | - name: Set up Python 3.11 19 | uses: actions/setup-python@v6.1.0 20 | with: 21 | python-version: 3.11 22 | - name: Cache 23 | uses: actions/cache@v4.3.0 24 | with: 25 | path: ~/.cache/pip 26 | key: pip-format 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip wheel 30 | python -m pip install --upgrade black isort 31 | - name: Pull again 32 | run: git pull || true 33 | - name: Run formatting 34 | run: | 35 | python -m isort -v --multi-line 3 --trailing-comma -l 88 --recursive . 36 | python -m black -v . 37 | - name: Commit files 38 | run: | 39 | if [ $(git diff HEAD | wc -l) -gt 30 ] 40 | then 41 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 42 | git config user.name "GitHub Actions" 43 | git commit -m "Run formatting" -a || true 44 | git push || true 45 | fi 46 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/nb.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Denne kontoen er allerede konfigurert" 5 | }, 6 | "error": { 7 | "cannot_connect": "Tilkobling mislyktes", 8 | "invalid_auth": "Feil ved autentisering", 9 | "unknown": "Uventet feil" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "email": "E-post", 15 | "password": "Passord", 16 | "type": "Modell" 17 | }, 18 | "title": "Koble til din Landroid Cloud-konto" 19 | } 20 | } 21 | }, 22 | "entity": { 23 | "lawn_mower": { 24 | "landroid_cloud": { 25 | "state": { 26 | "edgecut": "Klipper kant", 27 | "initializing": "Klargjør", 28 | "mowing": "Klipper", 29 | "offline": "Frakoblet", 30 | "rain_delay": "Regnforsinkelse", 31 | "starting": "Starter", 32 | "zoning": "Sonetrening" 33 | } 34 | } 35 | } 36 | }, 37 | "services": { 38 | "config": { 39 | "fields": { 40 | "raindelay": { 41 | "name": "Regnforsinkelse" 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | env: 9 | COMPONENT_DIR: landroid_cloud 10 | 11 | jobs: 12 | release_zip_file: 13 | name: Prepare release asset 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out repository 17 | uses: actions/checkout@v6 18 | - name: Update manifest.json version to ${{ github.event.release.tag_name }} 19 | run: | 20 | python3 ${{ github.workspace }}/.github/scripts/update_hacs_manifest.py --version ${{ github.event.release.tag_name }} --path /custom_components/landroid_cloud/ 21 | - name: Commit manifest update 22 | run: | 23 | git config user.name github-actions 24 | git config user.email github-actions@github.com 25 | git add ./custom_components/landroid_cloud/manifest.json 26 | git commit -m "Updated manifest.json" 27 | git push origin HEAD:master 28 | - name: Create zip 29 | run: | 30 | cd custom_components/landroid_cloud 31 | zip landroid_cloud.zip -r ./ 32 | - name: Upload zip to release 33 | uses: svenstaro/upload-release-action@2.11.3 34 | with: 35 | repo_token: ${{ secrets.GITHUB_TOKEN }} 36 | file: ./custom_components/landroid_cloud/landroid_cloud.zip 37 | asset_name: landroid_cloud.zip 38 | tag: ${{ github.ref }} 39 | overwrite: true 40 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/no.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Denne brukerkontoen er allerede konfigurert" 5 | }, 6 | "error": { 7 | "cannot_connect": "Tilkobling mislyktes", 8 | "invalid_auth": "Feil ved autentisering", 9 | "unknown": "Uventet feil" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "email": "E-post", 15 | "password": "Passord", 16 | "type": "Gjøre" 17 | }, 18 | "title": "Koble til din Landroid Cloud-konto" 19 | } 20 | } 21 | }, 22 | "entity": { 23 | "lawn_mower": { 24 | "landroid_cloud": { 25 | "state": { 26 | "edgecut": "Skjærekant", 27 | "initializing": "Initialiserer", 28 | "mowing": "Klipping", 29 | "offline": "Frakoblet", 30 | "rain_delay": "Forsinket regn", 31 | "starting": "Starter", 32 | "zoning": "Sonetrening" 33 | } 34 | } 35 | } 36 | }, 37 | "services": { 38 | "config": { 39 | "fields": { 40 | "raindelay": { 41 | "name": "Forsinket regn" 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | change-template: '- #$NUMBER $TITLE @$AUTHOR' 4 | sort-direction: ascending 5 | exclude-labels: 6 | - 'skip-changelog' 7 | categories: 8 | - title: '🛠 Breaking Changes' 9 | labels: 10 | - breaking-change 11 | 12 | - title: '🚀 Features' 13 | labels: 14 | - 'feature request' 15 | - 'enhancement' 16 | 17 | - title: '🐛 Bug Fixes' 18 | labels: 19 | - 'fix' 20 | - 'bugfix' 21 | - 'bug' 22 | 23 | - title: '🧰 Maintenance' 24 | label: 'chore' 25 | 26 | - title: ":package: Dependencies" 27 | labels: 28 | - 'dependencies' 29 | 30 | version-resolver: 31 | major: 32 | labels: 33 | - 'major' 34 | minor: 35 | labels: 36 | - 'minor' 37 | patch: 38 | labels: 39 | - 'patch' 40 | default: patch 41 | template: | 42 | ## Changes 43 | 44 | $CHANGES 45 | 46 | ## Say thanks 47 | 48 | Buy Me A Coffee 49 | 50 | autolabeler: 51 | - label: 'bug' 52 | branch: 53 | - '/fix\/.+/' 54 | - label: 'feature request' 55 | branch: 56 | - '/feature\/.+/' 57 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Эта учетная запись пользователя уже настроена" 5 | }, 6 | "error": { 7 | "cannot_connect": "Не удалось подключиться", 8 | "invalid_auth": "Ошибка аутентификации", 9 | "unknown": "Неожиданная ошибка" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "email": "Email", 15 | "password": "Пароль", 16 | "type": "Марка" 17 | }, 18 | "title": "Подключиться к учетной записи Landroid Cloud" 19 | } 20 | } 21 | }, 22 | "entity": { 23 | "lawn_mower": { 24 | "landroid_cloud": { 25 | "state": { 26 | "edgecut": "Обрезка края", 27 | "initializing": "Инициализация", 28 | "mowing": "Стрижка", 29 | "offline": "Не в сети", 30 | "rain_delay": "Задержка из-за дождя", 31 | "starting": "Старт", 32 | "zoning": "Поиск зоны" 33 | } 34 | } 35 | } 36 | }, 37 | "services": { 38 | "config": { 39 | "fields": { 40 | "raindelay": { 41 | "name": "Задержка из-за дождя" 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/select.py: -------------------------------------------------------------------------------- 1 | """Input select for landroid_cloud.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.const import EntityCategory 7 | from homeassistant.core import HomeAssistant 8 | 9 | from .api import LandroidAPI 10 | from .const import ATTR_DEVICES, DOMAIN 11 | from .device_base import LandroidSelect, LandroidSelectEntityDescription 12 | 13 | INPUT_SELECT = [ 14 | LandroidSelectEntityDescription( 15 | key="zoneselect", 16 | name="Current zone", 17 | entity_category=EntityCategory.CONFIG, 18 | device_class=None, 19 | entity_registry_enabled_default=True, 20 | unit_of_measurement=None, 21 | options=["1", "2", "3", "4"], 22 | value_fn=lambda device: device.zone.current, 23 | command_fn=lambda api, value: api.cloud.setzone( 24 | api.device.serial_number, value 25 | ), 26 | icon="mdi:map-clock", 27 | ), 28 | ] 29 | 30 | 31 | async def async_setup_entry( 32 | hass: HomeAssistant, 33 | config: ConfigEntry, 34 | async_add_devices, 35 | ) -> None: 36 | """Set up the switch platform.""" 37 | entities = [] 38 | for _, info in hass.data[DOMAIN][config.entry_id][ATTR_DEVICES].items(): 39 | api: LandroidAPI = info["api"] 40 | for select in INPUT_SELECT: 41 | entity = LandroidSelect(hass, select, api, config) 42 | entities.append(entity) 43 | 44 | async_add_devices(entities) 45 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Get diagnostics.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | 7 | from homeassistant.components.diagnostics import async_redact_data 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.const import ( 10 | CONF_EMAIL, 11 | CONF_LATITUDE, 12 | CONF_LONGITUDE, 13 | CONF_PASSWORD, 14 | CONF_TYPE, 15 | CONF_UNIQUE_ID, 16 | ) 17 | from homeassistant.core import HomeAssistant 18 | 19 | from .api import LandroidAPI 20 | from .const import ( 21 | ATTR_CLOUD, 22 | ATTR_DEVICEIDS, 23 | ATTR_DEVICES, 24 | ATTR_FEATUREBITS, 25 | DOMAIN, 26 | REDACT_TITLE, 27 | ) 28 | 29 | TO_REDACT = { 30 | CONF_LONGITUDE, 31 | CONF_LATITUDE, 32 | CONF_UNIQUE_ID, 33 | CONF_PASSWORD, 34 | CONF_EMAIL, 35 | REDACT_TITLE, 36 | } 37 | 38 | 39 | async def async_get_config_entry_diagnostics( 40 | hass: HomeAssistant, entry: ConfigEntry 41 | ) -> dict[str, Any]: 42 | """Return diagnostics for a config entry.""" 43 | data_entry = hass.data[DOMAIN][entry.entry_id] 44 | 45 | data_dict = { 46 | "entry": entry.as_dict(), 47 | ATTR_CLOUD: data_entry[ATTR_CLOUD], 48 | ATTR_DEVICEIDS: data_entry[ATTR_DEVICEIDS], 49 | ATTR_FEATUREBITS: data_entry[ATTR_FEATUREBITS], 50 | CONF_TYPE: data_entry[CONF_TYPE], 51 | } 52 | 53 | device_dict = {} 54 | for name, info in hass.data[DOMAIN][entry.entry_id][ATTR_DEVICES].items(): 55 | api: LandroidAPI = info["api"] 56 | device = {} 57 | for attr, value in api.device.__dict__.items(): 58 | device.update({attr: value}) 59 | 60 | device_dict.update({name: device}) 61 | 62 | data_dict.update({"devices": device_dict}) 63 | 64 | return async_redact_data(data_dict, TO_REDACT) 65 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml 2 | 3 | target-version = "py311" 4 | 5 | select = [ 6 | "B007", # Loop control variable {name} not used within loop body 7 | "B014", # Exception handler with duplicate exception 8 | "C", # complexity 9 | "D", # docstrings 10 | "E", # pycodestyle 11 | "F", # pyflakes/autoflake 12 | "ICN001", # import concentions; {name} should be imported as {asname} 13 | "PGH004", # Use specific rule codes when using noqa 14 | "PLC0414", # Useless import alias. Import alias does not rename original package. 15 | "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass 16 | "SIM117", # Merge with-statements that use the same scope 17 | "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() 18 | "SIM201", # Use {left} != {right} instead of not {left} == {right} 19 | "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} 20 | "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. 21 | "SIM401", # Use get from dict with default instead of an if block 22 | "T20", # flake8-print 23 | "TRY004", # Prefer TypeError exception for invalid type 24 | "RUF006", # Store a reference to the return value of asyncio.create_task 25 | "UP", # pyupgrade 26 | "W", # pycodestyle 27 | ] 28 | 29 | ignore = [ 30 | "D202", # No blank lines allowed after function docstring 31 | "D203", # 1 blank line required before class docstring 32 | "D213", # Multi-line docstring summary should start at the second line 33 | "D404", # First word of the docstring should not be This 34 | "D406", # Section name should end with a newline 35 | "D407", # Section name underlining 36 | "D411", # Missing blank line before section 37 | "E501", # line too long 38 | "E731", # do not assign a lambda expression, use a def 39 | "E722", # do not use bare `except` 40 | ] 41 | 42 | [flake8-pytest-style] 43 | fixture-parentheses = false 44 | 45 | [pyupgrade] 46 | keep-runtime-typing = true 47 | 48 | [mccabe] 49 | max-complexity = 25 -------------------------------------------------------------------------------- /custom_components/landroid_cloud/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Binary sensors for landroid_cloud.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.components.binary_sensor import BinarySensorDeviceClass 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.const import EntityCategory 8 | from homeassistant.core import HomeAssistant 9 | 10 | from .api import LandroidAPI 11 | from .const import ATTR_DEVICES, DOMAIN 12 | from .device_base import LandroidBinarySensor, LandroidBinarySensorEntityDescription 13 | 14 | BINARYSENSORS = [ 15 | LandroidBinarySensorEntityDescription( 16 | key="battery_charging", 17 | name="Battery Charging", 18 | entity_category=EntityCategory.DIAGNOSTIC, 19 | device_class=BinarySensorDeviceClass.BATTERY_CHARGING, 20 | entity_registry_enabled_default=True, 21 | value_fn=lambda landroid: landroid.battery.get("charging", False), 22 | ), 23 | LandroidBinarySensorEntityDescription( 24 | key="online", 25 | name="Online", 26 | entity_category=EntityCategory.DIAGNOSTIC, 27 | device_class=BinarySensorDeviceClass.CONNECTIVITY, 28 | entity_registry_enabled_default=True, 29 | value_fn=lambda landroid: landroid.online, 30 | ), 31 | LandroidBinarySensorEntityDescription( 32 | key="rainsensor_triggered", 33 | name="Rainsensor Triggered", 34 | entity_category=EntityCategory.DIAGNOSTIC, 35 | device_class=BinarySensorDeviceClass.MOISTURE, 36 | entity_registry_enabled_default=True, 37 | value_fn=lambda landroid: landroid.rainsensor.get("triggered", None), 38 | ), 39 | ] 40 | 41 | 42 | async def async_setup_entry( 43 | hass: HomeAssistant, 44 | config: ConfigEntry, 45 | async_add_devices, 46 | ) -> None: 47 | """Set up the binary_sensor platform.""" 48 | binarysensors = [] 49 | for _, info in hass.data[DOMAIN][config.entry_id][ATTR_DEVICES].items(): 50 | api: LandroidAPI = info["api"] 51 | for sens in BINARYSENSORS: 52 | entity = LandroidBinarySensor(hass, sens, api, config) 53 | 54 | binarysensors.append(entity) 55 | 56 | async_add_devices(binarysensors) 57 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/utils/schedules.py: -------------------------------------------------------------------------------- 1 | """Utilities used by this module.""" 2 | 3 | # pylint: disable=relative-beyond-top-level 4 | from __future__ import annotations 5 | 6 | import re 7 | from datetime import datetime 8 | 9 | from homeassistant.exceptions import HomeAssistantError 10 | 11 | TIME_REGEX = "(([0-9]){1,2}:([0-9]){2})" 12 | 13 | 14 | def parseday(day: dict, data: dict) -> list: 15 | """Parse a schedule day.""" 16 | result = [] 17 | start = re.search(TIME_REGEX, data[day["start"]].replace(".", ":")) 18 | end = re.search(TIME_REGEX, data[day["end"]].replace(".", ":")) 19 | 20 | if start: 21 | start = start.group(1) 22 | else: 23 | raise HomeAssistantError( 24 | f"Wrong format in {day['start']}, needs to be HH:MM format" 25 | ) 26 | 27 | if end: 28 | end = end.group(1) 29 | else: 30 | raise HomeAssistantError( 31 | f"Wrong format in {day['end']}, needs to be HH:MM format" 32 | ) 33 | 34 | if day["boundary"] in data: 35 | boundary = bool(data[day["boundary"]]) 36 | else: 37 | boundary = False 38 | 39 | result.append(start) 40 | time_start = datetime.strptime(start, "%H:%M") 41 | time_end = datetime.strptime(end, "%H:%M") 42 | runtime = (time_end - time_start).total_seconds() / 60 43 | result.append(int(runtime)) 44 | result.append(int(boundary)) 45 | 46 | if runtime == 0: 47 | result[0] = "00:00" 48 | result[2] = 0 49 | 50 | return result 51 | 52 | 53 | def pass_thru(schedule, sunday_first: bool = True) -> list: 54 | """Parse primary schedule thru, before generating secondary schedule.""" 55 | result = [] 56 | 57 | if sunday_first: 58 | result.append( 59 | [ 60 | schedule["sunday"]["start"], 61 | int(schedule["sunday"]["duration"]), 62 | int(schedule["sunday"]["boundary"]), 63 | ] 64 | ) 65 | 66 | for day in schedule.items(): 67 | if sunday_first and day[0] != "sunday": 68 | result.append( 69 | [day[1]["start"], int(day[1]["duration"]), int(day[1]["boundary"])] 70 | ) 71 | 72 | return result 73 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/devices/kress.py: -------------------------------------------------------------------------------- 1 | """Kress device definition.""" 2 | 3 | # pylint: disable=unused-argument,relative-beyond-top-level 4 | from __future__ import annotations 5 | 6 | import voluptuous as vol 7 | from homeassistant.components.lawn_mower import LawnMowerEntity 8 | 9 | from ..const import ( 10 | ATTR_BOUNDARY, 11 | ATTR_MULTIZONE_DISTANCES, 12 | ATTR_MULTIZONE_PROBABILITIES, 13 | ATTR_RAINDELAY, 14 | ATTR_RUNTIME, 15 | ATTR_TIMEEXTENSION, 16 | LandroidFeatureSupport, 17 | ) 18 | from ..device_base import SUPPORT_LANDROID_BASE, LandroidCloudMowerBase 19 | 20 | # from homeassistant.helpers.dispatcher import dispatcher_send 21 | 22 | 23 | SUPPORTED_FEATURES = SUPPORT_LANDROID_BASE 24 | 25 | DEVICE_FEATURES = ( 26 | LandroidFeatureSupport.MOWER 27 | | LandroidFeatureSupport.BUTTON 28 | | LandroidFeatureSupport.LOCK 29 | | LandroidFeatureSupport.CONFIG 30 | | LandroidFeatureSupport.RESTART 31 | | LandroidFeatureSupport.SELECT 32 | | LandroidFeatureSupport.SETZONE 33 | | LandroidFeatureSupport.SCHEDULES 34 | ) 35 | 36 | OTS_SCHEME = vol.Schema( 37 | { 38 | vol.Required(ATTR_BOUNDARY, default=False): bool, 39 | vol.Required(ATTR_RUNTIME, default=30): vol.Coerce(int), 40 | } 41 | ) 42 | 43 | CONFIG_SCHEME = vol.Schema( 44 | { 45 | vol.Optional(ATTR_RAINDELAY): vol.All(vol.Coerce(int), vol.Range(0, 300)), 46 | vol.Optional(ATTR_TIMEEXTENSION): vol.All( 47 | vol.Coerce(int), vol.Range(-100, 100) 48 | ), 49 | vol.Optional(ATTR_MULTIZONE_DISTANCES): str, 50 | vol.Optional(ATTR_MULTIZONE_PROBABILITIES): str, 51 | } 52 | ) 53 | 54 | 55 | class MowerDevice(LandroidCloudMowerBase, LawnMowerEntity): 56 | """Definition of Kress device.""" 57 | 58 | @property 59 | def base_features(self): 60 | """Flag which Landroid Cloud specific features that are supported.""" 61 | return DEVICE_FEATURES 62 | 63 | @property 64 | def supported_features(self): 65 | """Flag which mower robot features that are supported.""" 66 | return SUPPORTED_FEATURES 67 | 68 | @staticmethod 69 | def get_ots_scheme(): 70 | """Get device specific OTS_SCHEME.""" 71 | return OTS_SCHEME 72 | 73 | @staticmethod 74 | def get_config_scheme(): 75 | """Get device specific CONFIG_SCHEME.""" 76 | return CONFIG_SCHEME 77 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/devices/ferrex.py: -------------------------------------------------------------------------------- 1 | """Aldi Ferrex device definition.""" 2 | 3 | # pylint: disable=unused-argument,relative-beyond-top-level 4 | from __future__ import annotations 5 | 6 | import voluptuous as vol 7 | from homeassistant.components.lawn_mower import LawnMowerEntity 8 | 9 | from ..const import ( 10 | ATTR_BOUNDARY, 11 | ATTR_MULTIZONE_DISTANCES, 12 | ATTR_MULTIZONE_PROBABILITIES, 13 | ATTR_RAINDELAY, 14 | ATTR_RUNTIME, 15 | ATTR_TIMEEXTENSION, 16 | LandroidFeatureSupport, 17 | ) 18 | from ..device_base import SUPPORT_LANDROID_BASE, LandroidCloudMowerBase 19 | 20 | # from homeassistant.helpers.dispatcher import dispatcher_send 21 | 22 | 23 | SUPPORTED_FEATURES = SUPPORT_LANDROID_BASE 24 | 25 | DEVICE_FEATURES = ( 26 | LandroidFeatureSupport.MOWER 27 | | LandroidFeatureSupport.BUTTON 28 | | LandroidFeatureSupport.LOCK 29 | | LandroidFeatureSupport.CONFIG 30 | | LandroidFeatureSupport.RESTART 31 | | LandroidFeatureSupport.SELECT 32 | | LandroidFeatureSupport.SETZONE 33 | | LandroidFeatureSupport.SCHEDULES 34 | ) 35 | 36 | OTS_SCHEME = vol.Schema( 37 | { 38 | vol.Required(ATTR_BOUNDARY, default=False): bool, 39 | vol.Required(ATTR_RUNTIME, default=30): vol.Coerce(int), 40 | } 41 | ) 42 | 43 | CONFIG_SCHEME = vol.Schema( 44 | { 45 | vol.Optional(ATTR_RAINDELAY): vol.All(vol.Coerce(int), vol.Range(0, 300)), 46 | vol.Optional(ATTR_TIMEEXTENSION): vol.All( 47 | vol.Coerce(int), vol.Range(-100, 100) 48 | ), 49 | vol.Optional(ATTR_MULTIZONE_DISTANCES): str, 50 | vol.Optional(ATTR_MULTIZONE_PROBABILITIES): str, 51 | } 52 | ) 53 | 54 | 55 | class MowerDevice(LandroidCloudMowerBase, LawnMowerEntity): 56 | """Definition of KreAldi Ferrexss device.""" 57 | 58 | @property 59 | def base_features(self): 60 | """Flag which Landroid Cloud specific features that are supported.""" 61 | return DEVICE_FEATURES 62 | 63 | @property 64 | def supported_features(self): 65 | """Flag which mower robot features that are supported.""" 66 | return SUPPORTED_FEATURES 67 | 68 | @staticmethod 69 | def get_ots_scheme(): 70 | """Get device specific OTS_SCHEME.""" 71 | return OTS_SCHEME 72 | 73 | @staticmethod 74 | def get_config_scheme(): 75 | """Get device specific CONFIG_SCHEME.""" 76 | return CONFIG_SCHEME 77 | -------------------------------------------------------------------------------- /.github/scripts/update_hacs_manifest.py: -------------------------------------------------------------------------------- 1 | """Update the manifest file.""" 2 | 3 | import json 4 | import os 5 | import sys 6 | 7 | 8 | def update_manifest(): 9 | """Update the manifest file.""" 10 | version = "0.0.0" 11 | manifest_path = False 12 | dorequirements = False 13 | 14 | for index, value in enumerate(sys.argv): 15 | if value in ["--version", "-V"]: 16 | version = str(sys.argv[index + 1]).replace("v", "") 17 | if value in ["--path", "-P"]: 18 | manifest_path = str(sys.argv[index + 1])[1:-1] 19 | if value in ["--requirements", "-R"]: 20 | dorequirements = True 21 | 22 | if not manifest_path: 23 | sys.exit("Missing path to manifest file") 24 | 25 | with open( 26 | f"{os.getcwd()}/{manifest_path}/manifest.json", 27 | encoding="UTF-8", 28 | ) as manifestfile: 29 | manifest = json.load(manifestfile) 30 | 31 | manifest["version"] = version 32 | 33 | if dorequirements: 34 | requirements = [] 35 | with open( 36 | f"{os.getcwd()}/requirements.txt", 37 | encoding="UTF-8", 38 | ) as file: 39 | for line in file: 40 | requirements.append(line.rstrip()) 41 | 42 | new_requirements = [] 43 | for requirement in requirements: 44 | req = requirement.split("==")[0].lower() 45 | new_requirements = [ 46 | requirement 47 | for x in manifest["requirements"] 48 | if x.lower().startswith(req) 49 | ] 50 | new_requirements += [ 51 | x for x in manifest["requirements"] if not x.lower().startswith(req) 52 | ] 53 | manifest["requirements"] = new_requirements 54 | 55 | with open( 56 | f"{os.getcwd()}/{manifest_path}/manifest.json", 57 | "w", 58 | encoding="UTF-8", 59 | ) as manifestfile: 60 | manifestfile.write( 61 | json.dumps( 62 | { 63 | "domain": manifest["domain"], 64 | "name": manifest["name"], 65 | **{ 66 | k: v 67 | for k, v in sorted(manifest.items()) 68 | if k not in ("domain", "name") 69 | }, 70 | }, 71 | indent=4, 72 | ) 73 | ) 74 | 75 | 76 | update_manifest() 77 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/devices/landxcape.py: -------------------------------------------------------------------------------- 1 | """LandXcape device definition.""" 2 | 3 | # pylint: disable=unused-argument,relative-beyond-top-level 4 | from __future__ import annotations 5 | 6 | import voluptuous as vol 7 | from homeassistant.components.lawn_mower import LawnMowerEntity 8 | from pyworxcloud import DeviceHandler 9 | 10 | from ..const import ( 11 | ATTR_BOUNDARY, 12 | ATTR_MULTIZONE_DISTANCES, 13 | ATTR_MULTIZONE_PROBABILITIES, 14 | ATTR_RAINDELAY, 15 | ATTR_RUNTIME, 16 | ATTR_TIMEEXTENSION, 17 | LandroidFeatureSupport, 18 | ) 19 | from ..device_base import SUPPORT_LANDROID_BASE, LandroidCloudMowerBase 20 | 21 | # from homeassistant.helpers.dispatcher import dispatcher_send 22 | 23 | 24 | SUPPORTED_FEATURES = SUPPORT_LANDROID_BASE 25 | 26 | CONFIG_SCHEME = vol.Schema( 27 | { 28 | vol.Optional(ATTR_RAINDELAY): vol.All(vol.Coerce(int), vol.Range(0, 300)), 29 | vol.Optional(ATTR_TIMEEXTENSION): vol.All( 30 | vol.Coerce(int), vol.Range(-100, 100) 31 | ), 32 | vol.Optional(ATTR_MULTIZONE_DISTANCES): str, 33 | vol.Optional(ATTR_MULTIZONE_PROBABILITIES): str, 34 | } 35 | ) 36 | 37 | OTS_SCHEME = vol.Schema( 38 | { 39 | vol.Required(ATTR_BOUNDARY, default=False): bool, 40 | vol.Required(ATTR_RUNTIME, default=30): vol.Coerce(int), 41 | } 42 | ) 43 | 44 | DEVICE_FEATURES = ( 45 | LandroidFeatureSupport.MOWER 46 | | LandroidFeatureSupport.BUTTON 47 | | LandroidFeatureSupport.LOCK 48 | | LandroidFeatureSupport.CONFIG 49 | | LandroidFeatureSupport.RESTART 50 | | LandroidFeatureSupport.SELECT 51 | | LandroidFeatureSupport.SETZONE 52 | | LandroidFeatureSupport.SCHEDULES 53 | ) 54 | 55 | 56 | class MowerDevice(LandroidCloudMowerBase, LawnMowerEntity): 57 | """Definition of Landxcape device.""" 58 | 59 | def __init__(self, hass, api): 60 | """Initialize mower entity.""" 61 | super().__init__(hass, api) 62 | self.device: DeviceHandler = self._device 63 | 64 | @property 65 | def base_features(self): 66 | """Flag which Landroid Cloud specific features that are supported.""" 67 | return DEVICE_FEATURES 68 | 69 | @property 70 | def supported_features(self): 71 | """Flag which mower robot features that are supported.""" 72 | return SUPPORTED_FEATURES 73 | 74 | @staticmethod 75 | def get_ots_scheme(): 76 | """Get device specific OTS_SCHEME.""" 77 | return OTS_SCHEME 78 | 79 | @staticmethod 80 | def get_config_scheme(): 81 | """Get device specific CONFIG_SCHEME.""" 82 | return CONFIG_SCHEME 83 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/device_action.py: -------------------------------------------------------------------------------- 1 | """Provides device automations for Vacuum.""" 2 | 3 | from __future__ import annotations 4 | 5 | import voluptuous as vol 6 | from homeassistant.components.device_automation import async_validate_entity_schema 7 | from homeassistant.components.lawn_mower.const import ( 8 | SERVICE_DOCK, 9 | SERVICE_PAUSE, 10 | SERVICE_START_MOWING, 11 | ) 12 | from homeassistant.const import ( 13 | ATTR_ENTITY_ID, 14 | CONF_DEVICE_ID, 15 | CONF_DOMAIN, 16 | CONF_ENTITY_ID, 17 | CONF_TYPE, 18 | ) 19 | from homeassistant.core import Context, HomeAssistant 20 | from homeassistant.helpers import config_validation as cv 21 | from homeassistant.helpers import entity_registry as er 22 | from homeassistant.helpers.typing import ConfigType, TemplateVarsType 23 | 24 | from . import DOMAIN 25 | 26 | MOWER_DOMAIN = "lawn_mower" 27 | 28 | ACTION_TYPES = {"mow", "dock", "pause"} 29 | 30 | _ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( 31 | { 32 | vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), 33 | vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, 34 | } 35 | ) 36 | 37 | 38 | async def async_validate_action_config( 39 | hass: HomeAssistant, config: ConfigType 40 | ) -> ConfigType: 41 | """Validate config.""" 42 | return async_validate_entity_schema(hass, config, _ACTION_SCHEMA) 43 | 44 | 45 | async def async_get_actions( 46 | hass: HomeAssistant, device_id: str 47 | ) -> list[dict[str, str]]: 48 | """List device actions for Vacuum devices.""" 49 | registry = er.async_get(hass) 50 | actions = [] 51 | 52 | # Get all the integrations entities for this device 53 | for entry in er.async_entries_for_device(registry, device_id): 54 | if entry.domain != MOWER_DOMAIN: 55 | continue 56 | 57 | base_action = { 58 | CONF_DEVICE_ID: device_id, 59 | CONF_DOMAIN: DOMAIN, 60 | CONF_ENTITY_ID: entry.id, 61 | } 62 | 63 | actions += [{**base_action, CONF_TYPE: action} for action in ACTION_TYPES] 64 | 65 | return actions 66 | 67 | 68 | async def async_call_action_from_config( 69 | hass: HomeAssistant, 70 | config: ConfigType, 71 | variables: TemplateVarsType, 72 | context: Context | None, 73 | ) -> None: 74 | """Execute a device action.""" 75 | service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} 76 | 77 | if config[CONF_TYPE] == "mow": 78 | service = SERVICE_START_MOWING 79 | service_domain = MOWER_DOMAIN 80 | elif config[CONF_TYPE] == "dock": 81 | service = SERVICE_DOCK 82 | service_domain = MOWER_DOMAIN 83 | elif config[CONF_TYPE] == "pause": 84 | service = SERVICE_PAUSE 85 | service_domain = MOWER_DOMAIN 86 | 87 | await hass.services.async_call( 88 | service_domain, service, service_data, blocking=True, context=context 89 | ) 90 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/scheme.py: -------------------------------------------------------------------------------- 1 | """Data schemes used by Landroid Cloud integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import voluptuous as vol 6 | from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TYPE 7 | from homeassistant.helpers import config_validation as cv 8 | 9 | from .const import ( 10 | ATTR_FRIDAY_BOUNDARY, 11 | ATTR_FRIDAY_END, 12 | ATTR_FRIDAY_START, 13 | ATTR_JSON, 14 | ATTR_MONDAY_BOUNDARY, 15 | ATTR_MONDAY_END, 16 | ATTR_MONDAY_START, 17 | ATTR_SATURDAY_BOUNDARY, 18 | ATTR_SATURDAY_END, 19 | ATTR_SATURDAY_START, 20 | ATTR_SUNDAY_BOUNDARY, 21 | ATTR_SUNDAY_END, 22 | ATTR_SUNDAY_START, 23 | ATTR_THURSDAY_BOUNDARY, 24 | ATTR_THURSDAY_END, 25 | ATTR_THURSDAY_START, 26 | ATTR_TUESDAY_BOUNDARY, 27 | ATTR_TUESDAY_END, 28 | ATTR_TUESDAY_START, 29 | ATTR_TYPE, 30 | ATTR_WEDNESDAY_BOUNDARY, 31 | ATTR_WEDNESDAY_END, 32 | ATTR_WEDNESDAY_START, 33 | CLOUDS, 34 | DOMAIN, 35 | ) 36 | 37 | DATA_SCHEMA = vol.Schema( 38 | { 39 | vol.Required(CONF_EMAIL): str, 40 | vol.Required(CONF_PASSWORD): str, 41 | vol.Optional( 42 | CONF_TYPE, default=[x for x in CLOUDS if x.lower() == "worx"][0] 43 | ): vol.In(CLOUDS), 44 | } 45 | ) 46 | 47 | EMPTY_SCHEME = vol.All(cv.make_entity_service_schema({})) 48 | 49 | CONFIG_SCHEMA = vol.Schema( 50 | { 51 | DOMAIN: vol.All( 52 | cv.ensure_list, 53 | [DATA_SCHEMA], 54 | ) 55 | }, 56 | extra=vol.ALLOW_EXTRA, 57 | ) 58 | 59 | SCHEDULE_SCHEME = vol.Schema( 60 | { 61 | vol.Required(ATTR_TYPE, default="primary"): vol.In(["primary", "secondary"]), 62 | vol.Optional(ATTR_MONDAY_START): str, 63 | vol.Optional(ATTR_MONDAY_END): str, 64 | vol.Optional(ATTR_MONDAY_BOUNDARY): bool, 65 | vol.Optional(ATTR_TUESDAY_START): str, 66 | vol.Optional(ATTR_TUESDAY_END): str, 67 | vol.Optional(ATTR_TUESDAY_BOUNDARY): bool, 68 | vol.Optional(ATTR_WEDNESDAY_START): str, 69 | vol.Optional(ATTR_WEDNESDAY_END): str, 70 | vol.Optional(ATTR_WEDNESDAY_BOUNDARY): bool, 71 | vol.Optional(ATTR_THURSDAY_START): str, 72 | vol.Optional(ATTR_THURSDAY_END): str, 73 | vol.Optional(ATTR_THURSDAY_BOUNDARY): bool, 74 | vol.Optional(ATTR_FRIDAY_START): str, 75 | vol.Optional(ATTR_FRIDAY_END): str, 76 | vol.Optional(ATTR_FRIDAY_BOUNDARY): bool, 77 | vol.Optional(ATTR_SATURDAY_START): str, 78 | vol.Optional(ATTR_SATURDAY_END): str, 79 | vol.Optional(ATTR_SATURDAY_BOUNDARY): bool, 80 | vol.Optional(ATTR_SUNDAY_START): str, 81 | vol.Optional(ATTR_SUNDAY_END): str, 82 | vol.Optional(ATTR_SUNDAY_BOUNDARY): bool, 83 | }, 84 | extra=vol.ALLOW_EXTRA, 85 | ) 86 | 87 | RAW_SCHEME = vol.Schema( 88 | { 89 | vol.Required(ATTR_JSON): str, 90 | }, 91 | extra=vol.ALLOW_EXTRA, 92 | ) 93 | 94 | OTS_SCHEME = "" 95 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/device_condition.py: -------------------------------------------------------------------------------- 1 | """Provide the device automations for Vacuum.""" 2 | 3 | from __future__ import annotations 4 | 5 | import voluptuous as vol 6 | from homeassistant.components.lawn_mower.const import LawnMowerActivity 7 | from homeassistant.const import ( 8 | CONF_CONDITION, 9 | CONF_DEVICE_ID, 10 | CONF_DOMAIN, 11 | CONF_ENTITY_ID, 12 | CONF_TYPE, 13 | ) 14 | from homeassistant.core import HomeAssistant, callback 15 | from homeassistant.helpers import condition 16 | from homeassistant.helpers import config_validation as cv 17 | from homeassistant.helpers import entity_registry as er 18 | from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA 19 | from homeassistant.helpers.typing import ConfigType, TemplateVarsType 20 | 21 | from custom_components.landroid_cloud.const import STATE_EDGECUT 22 | 23 | from . import DOMAIN 24 | 25 | MOWER_DOMAIN = "lawn_mower" 26 | 27 | CONDITION_TYPES = {"is_mowing", "is_docked", "is_edgecut", "has_error", "is_returning"} 28 | 29 | CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( 30 | { 31 | vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, 32 | vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), 33 | } 34 | ) 35 | 36 | 37 | async def async_get_conditions( 38 | hass: HomeAssistant, device_id: str 39 | ) -> list[dict[str, str]]: 40 | """List device conditions for Landroid Cloud devices.""" 41 | registry = er.async_get(hass) 42 | conditions = [] 43 | 44 | # Get all the integrations entities for this device 45 | for entry in er.async_entries_for_device(registry, device_id): 46 | if entry.domain != MOWER_DOMAIN: 47 | continue 48 | 49 | base_condition = { 50 | CONF_CONDITION: "device", 51 | CONF_DEVICE_ID: device_id, 52 | CONF_DOMAIN: DOMAIN, 53 | CONF_ENTITY_ID: entry.id, 54 | } 55 | 56 | conditions += [{**base_condition, CONF_TYPE: cond} for cond in CONDITION_TYPES] 57 | 58 | return conditions 59 | 60 | 61 | @callback 62 | def async_condition_from_config( 63 | hass: HomeAssistant, config: ConfigType 64 | ) -> condition.ConditionCheckerType: 65 | """Create a function to test a device condition.""" 66 | if config[CONF_TYPE] == "is_docked": 67 | test_states = [LawnMowerActivity.DOCKED] 68 | elif config[CONF_TYPE] == "is_mowing": 69 | test_states = [LawnMowerActivity.MOWING] 70 | elif config[CONF_TYPE] == "is_edgecut": 71 | test_states = [STATE_EDGECUT] 72 | elif config[CONF_TYPE] == "has_error": 73 | test_states = [LawnMowerActivity.ERROR] 74 | elif config[CONF_TYPE] == "is_returning": 75 | test_states = [LawnMowerActivity.RETURNING] 76 | 77 | registry = er.async_get(hass) 78 | entity_id = er.async_resolve_entity_id(registry, config[CONF_ENTITY_ID]) 79 | 80 | def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: 81 | """Test if an entity is a certain state.""" 82 | return ( 83 | entity_id is not None 84 | and (state := hass.states.get(entity_id)) is not None 85 | and state.state in test_states 86 | ) 87 | 88 | return test_is_state 89 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/button.py: -------------------------------------------------------------------------------- 1 | """Representation of a button.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.components.button import ButtonDeviceClass 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.helpers.entity import EntityCategory 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | 11 | from .api import LandroidAPI 12 | from .const import ATTR_DEVICES, DOMAIN, LOGLEVEL, LandroidFeatureSupport 13 | from .device_base import LandroidButton, LandroidButtonEntityDescription 14 | from .utils.logger import LandroidLogger, LoggerType 15 | 16 | # Tuple containing buttons to create 17 | BUTTONS = [ 18 | LandroidButtonEntityDescription( 19 | key="restart", 20 | name="Restart baseboard", 21 | icon="mdi:restart", 22 | entity_category=EntityCategory.CONFIG, 23 | device_class=ButtonDeviceClass.RESTART, 24 | required_feature=None, 25 | press_action=lambda api, serial: api.cloud.restart(serial), 26 | ), 27 | LandroidButtonEntityDescription( 28 | key="edgecut", 29 | name="Start cutting edge", 30 | icon="mdi:map-marker-path", 31 | entity_category=None, 32 | required_feature=LandroidFeatureSupport.EDGECUT, 33 | press_action=lambda api, serial: api.cloud.edgecut(serial), 34 | ), 35 | LandroidButtonEntityDescription( 36 | key="reset_charge_cycles", 37 | name="Reset charge cycles", 38 | icon="mdi:battery-sync", 39 | entity_category=EntityCategory.DIAGNOSTIC, 40 | required_feature=None, 41 | press_action=lambda api, serial: api.cloud.reset_charge_cycle_counter(serial), 42 | ), 43 | LandroidButtonEntityDescription( 44 | key="reset_blade_time", 45 | name="Reset blade time", 46 | icon="mdi:battery-sync", 47 | entity_category=EntityCategory.DIAGNOSTIC, 48 | required_feature=None, 49 | press_action=lambda api, serial: api.cloud.reset_blade_counter(serial), 50 | ), 51 | LandroidButtonEntityDescription( 52 | key="request_update", 53 | name="Request update", 54 | icon="mdi:refresh", 55 | entity_category=EntityCategory.DIAGNOSTIC, 56 | required_feature=None, 57 | entity_registry_enabled_default=False, 58 | press_action=lambda api, serial: api.cloud.update(serial), 59 | ), 60 | ] 61 | 62 | 63 | async def async_setup_entry( 64 | hass: HomeAssistant, 65 | config: ConfigEntry, 66 | async_add_entities: AddEntitiesCallback, 67 | ) -> None: 68 | """Set up Landroid buttons for specific service.""" 69 | entities = [] 70 | for _, info in hass.data[DOMAIN][config.entry_id][ATTR_DEVICES].items(): 71 | api: LandroidAPI = info["api"] 72 | logger = LandroidLogger(name=__name__, api=api, log_level=LOGLEVEL) 73 | 74 | for button in BUTTONS: 75 | if isinstance(button.required_feature, type(None)) or ( 76 | api.features & button.required_feature 77 | ): 78 | logger.log(LoggerType.FEATURE, "Adding %s button", button.key) 79 | entity = LandroidButton(hass, button, api, config) 80 | 81 | entities.append(entity) 82 | 83 | async_add_entities(entities, True) 84 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/devices/worx.py: -------------------------------------------------------------------------------- 1 | """Worx Landroid device definition.""" 2 | 3 | # pylint: disable=unused-argument,relative-beyond-top-level 4 | from __future__ import annotations 5 | 6 | import json 7 | 8 | import voluptuous as vol 9 | from homeassistant.components.lawn_mower import LawnMowerEntity 10 | from pyworxcloud import DeviceHandler 11 | 12 | from ..const import ( 13 | ATTR_BOUNDARY, 14 | ATTR_MULTIZONE_DISTANCES, 15 | ATTR_MULTIZONE_PROBABILITIES, 16 | ATTR_RAINDELAY, 17 | ATTR_RUNTIME, 18 | ATTR_TIMEEXTENSION, 19 | ATTR_TORQUE, 20 | LandroidFeatureSupport, 21 | ) 22 | from ..device_base import SUPPORT_LANDROID_BASE, LandroidCloudMowerBase 23 | from ..utils.logger import LoggerType 24 | 25 | # from homeassistant.helpers.dispatcher import dispatcher_send 26 | 27 | 28 | SUPPORTED_FEATURES = SUPPORT_LANDROID_BASE 29 | 30 | CONFIG_SCHEME = vol.Schema( 31 | { 32 | vol.Optional(ATTR_RAINDELAY): vol.All(vol.Coerce(int), vol.Range(0, 300)), 33 | vol.Optional(ATTR_TIMEEXTENSION): vol.All( 34 | vol.Coerce(int), vol.Range(-100, 100) 35 | ), 36 | vol.Optional(ATTR_MULTIZONE_DISTANCES): str, 37 | vol.Optional(ATTR_MULTIZONE_PROBABILITIES): str, 38 | } 39 | ) 40 | 41 | OTS_SCHEME = vol.Schema( 42 | { 43 | vol.Required(ATTR_BOUNDARY, default=False): bool, 44 | vol.Required(ATTR_RUNTIME, default=30): vol.Coerce(int), 45 | } 46 | ) 47 | 48 | DEVICE_FEATURES = ( 49 | LandroidFeatureSupport.MOWER 50 | | LandroidFeatureSupport.BUTTON 51 | | LandroidFeatureSupport.SELECT 52 | | LandroidFeatureSupport.LOCK 53 | | LandroidFeatureSupport.CONFIG 54 | | LandroidFeatureSupport.RESTART 55 | | LandroidFeatureSupport.SETZONE 56 | | LandroidFeatureSupport.RAW 57 | | LandroidFeatureSupport.SCHEDULES 58 | ) 59 | 60 | 61 | class MowerDevice(LandroidCloudMowerBase, LawnMowerEntity): 62 | """Definition of Worx Landroid device.""" 63 | 64 | def __init__(self, hass, api): 65 | """Initialize mower entity.""" 66 | super().__init__(hass, api) 67 | self.device: DeviceHandler = self._device 68 | 69 | @property 70 | def base_features(self): 71 | """Flag which Landroid Cloud specific features that are supported.""" 72 | return DEVICE_FEATURES 73 | 74 | @property 75 | def supported_features(self): 76 | """Flag which mower robot features that are supported.""" 77 | return SUPPORTED_FEATURES 78 | 79 | @staticmethod 80 | def get_ots_scheme(): 81 | """Get device specific OTS_SCHEME.""" 82 | return OTS_SCHEME 83 | 84 | @staticmethod 85 | def get_config_scheme(): 86 | """Get device specific CONFIG_SCHEME.""" 87 | return CONFIG_SCHEME 88 | 89 | async def async_set_torque(self, data: dict | None = None) -> None: 90 | """Set wheel torque.""" 91 | device: DeviceHandler = self._device 92 | self.log( 93 | LoggerType.SERVICE_CALL, 94 | "Setting wheel torque to %s", 95 | data[ATTR_TORQUE], 96 | ) 97 | tmpdata = {"tq": data[ATTR_TORQUE]} 98 | await self.hass.async_add_executor_job( 99 | self._api.cloud.send, device.serial_number, json.dumps(tmpdata) 100 | ) 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![landroid_cloud](https://img.shields.io/github/release/mtrab/landroid_cloud/all.svg?style=plastic&label=Current%20release)](https://github.com/mtrab/landroid_cloud) [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=plastic)](https://github.com/hacs/integration) [![downloads](https://img.shields.io/github/downloads/mtrab/landroid_cloud/total?style=plastic&label=Total%20downloads)](https://github.com/mtrab/landroid_cloud)
2 | [![Lokalize translation](https://img.shields.io/static/v1?label=Help%20translate&message=using%20Lokalize&color=green&style=plastic)](https://app.lokalise.com/public/38508561643d2bcfb05550.72266746/) [![Buy me a coffee](https://img.shields.io/static/v1?label=Buy%20me%20a%20coffee&message=and%20say%20thanks&color=orange&logo=buymeacoffee&logoColor=white&style=plastic)](https://www.buymeacoffee.com/mtrab) 3 | 4 | # Landroid Cloud 5 | 6 | This component has been created to be used with Home Assistant. 7 | 8 | Landroid Cloud presents a possibility to connect your Landroid Cloud compatible mowers to Home Assistant.
9 | Currently these vendors are supported:
10 | - Worx Landroid 11 | - Kress 12 | - LandXcape 13 | 14 | ### Installation: 15 | 16 | #### HACS 17 | 18 | - Ensure that HACS is installed. 19 | - Search for and install the "Landroid Cloud" integration. 20 | - Restart Home Assistant. 21 | - Go to Integrations and add the Landroid Cloud integration 22 | 23 | #### Manual installation 24 | 25 | - Download the latest release. 26 | - Unpack the release and copy the custom_components/landroid_cloud directory into the custom_components directory of your Home Assistant installation. 27 | - Restart Home Assistant. 28 | - Go to Integrations and add the Landroid Cloud integration 29 | 30 | ### Landroid Card 31 | 32 | [Barma-lej](https://github.com/barma-lej) has created a custom card for the Landroid Cloud integration.
33 | You can find installation instructions on [this Github repo](https://github.com/Barma-lej/landroid-card) 34 | 35 | ### Translation 36 | 37 | To handle submissions of translated strings I'm using [Lokalise](https://lokalise.com/).
38 | They provide an amazing platform that is easy to use and maintain.
39 |
40 | To help out with the translation of this custom_component you need an account on Lokalise.
41 | The easiest way to get one is to [click here](https://lokalise.com/login/) then select "Log in with GitHub".
42 |
43 | When you have created your account, [clich here](https://app.lokalise.com/public/38508561643d2bcfb05550.72266746/) to join the project on Lokalise.
44 |
45 | Check Lokalise documentation [here](https://docs.lokalise.com/en/) - it's really good.
46 |
47 | Can't find the language you want to translate to? [Open a new language request](https://github.com/MTrab/landroid_cloud/issues/new?assignees=&labels=translation&template=translation_request.md&title=%5BLR%5D%3A+New%20language%20request)
48 |
49 | Contributions to the translations will be updated on every release of this component. 50 | 51 | 52 | ### Other useful information 53 | #### Services and app stopped working 54 | 55 | You might experience being banned from Worx Landroid Cloud service. 56 | Follow this simple guide to make it work again: 57 | * Go to [My Landroids](https://account.worxlandroid.com/product-items) 58 | * Unlink your Landroid(s) 59 | * Open app on mobile device 60 | * Add Landroid(s) 61 | 62 | ### To-do 63 | 64 | * Make this an official integration (far in the future as there is to many changes right now) 65 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/device_trigger.py: -------------------------------------------------------------------------------- 1 | """Provides device automations for Landroid Cloud devices.""" 2 | 3 | from __future__ import annotations 4 | 5 | import voluptuous as vol 6 | from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA 7 | from homeassistant.components.homeassistant.triggers import state as state_trigger 8 | from homeassistant.components.lawn_mower.const import LawnMowerActivity 9 | from homeassistant.const import ( 10 | CONF_DEVICE_ID, 11 | CONF_DOMAIN, 12 | CONF_ENTITY_ID, 13 | CONF_FOR, 14 | CONF_PLATFORM, 15 | CONF_TYPE, 16 | ) 17 | from homeassistant.core import CALLBACK_TYPE, HomeAssistant 18 | from homeassistant.helpers import config_validation as cv 19 | from homeassistant.helpers import entity_registry as er 20 | from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo 21 | from homeassistant.helpers.typing import ConfigType 22 | 23 | from .const import DOMAIN, STATE_EDGECUT 24 | 25 | MOWER_DOMAIN = "lawn_mower" 26 | 27 | TRIGGER_TYPES = {"mowing", "docked", "edgecut", "error"} 28 | 29 | TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( 30 | { 31 | vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid, 32 | vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), 33 | vol.Optional(CONF_FOR): cv.positive_time_period_dict, 34 | } 35 | ) 36 | 37 | 38 | async def async_get_triggers( 39 | hass: HomeAssistant, device_id: str 40 | ) -> list[dict[str, str]]: 41 | """List device triggers for Landroid Cloud devices.""" 42 | registry = er.async_get(hass) 43 | triggers = [] 44 | 45 | # Get all the integrations entities for this device 46 | for entry in er.async_entries_for_device(registry, device_id): 47 | if entry.domain != MOWER_DOMAIN: 48 | continue 49 | 50 | triggers += [ 51 | { 52 | CONF_PLATFORM: "device", 53 | CONF_DEVICE_ID: device_id, 54 | CONF_DOMAIN: DOMAIN, 55 | CONF_ENTITY_ID: entry.id, 56 | CONF_TYPE: trigger, 57 | } 58 | for trigger in TRIGGER_TYPES 59 | ] 60 | 61 | return triggers 62 | 63 | 64 | async def async_get_trigger_capabilities( 65 | hass: HomeAssistant, config: ConfigType 66 | ) -> dict[str, vol.Schema]: 67 | """List trigger capabilities.""" 68 | return { 69 | "extra_fields": vol.Schema( 70 | {vol.Optional(CONF_FOR): cv.positive_time_period_dict} 71 | ) 72 | } 73 | 74 | 75 | async def async_attach_trigger( 76 | hass: HomeAssistant, 77 | config: ConfigType, 78 | action: TriggerActionType, 79 | trigger_info: TriggerInfo, 80 | ) -> CALLBACK_TYPE: 81 | """Attach a trigger.""" 82 | if config[CONF_TYPE] == "mowing": 83 | to_state = LawnMowerActivity.MOWING 84 | elif config[CONF_TYPE] == "docked": 85 | to_state = LawnMowerActivity.DOCKED 86 | elif config[CONF_TYPE] == "edgecut": 87 | to_state = STATE_EDGECUT 88 | elif config[CONF_TYPE] == "error": 89 | to_state = LawnMowerActivity.ERROR 90 | 91 | state_config = { 92 | CONF_PLATFORM: "state", 93 | CONF_ENTITY_ID: config[CONF_ENTITY_ID], 94 | state_trigger.CONF_TO: to_state, 95 | } 96 | if CONF_FOR in config: 97 | state_config[CONF_FOR] = config[CONF_FOR] 98 | state_config = await state_trigger.async_validate_trigger_config(hass, state_config) 99 | return await state_trigger.async_attach_trigger( 100 | hass, state_config, action, trigger_info, platform_type="device" 101 | ) 102 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/switch.py: -------------------------------------------------------------------------------- 1 | """Switches for landroid_cloud.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.components.switch import SwitchDeviceClass 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.const import EntityCategory 8 | from homeassistant.core import HomeAssistant 9 | 10 | from .api import LandroidAPI 11 | from .const import ATTR_DEVICES, DOMAIN, LOGLEVEL, LandroidFeatureSupport 12 | from .device_base import LandroidSwitch, LandroidSwitchEntityDescription 13 | from .utils.logger import LandroidLogger, LoggerType 14 | 15 | SWITCHES = [ 16 | LandroidSwitchEntityDescription( 17 | key="partymode", 18 | name="Party Mode", 19 | entity_category=EntityCategory.CONFIG, 20 | device_class=SwitchDeviceClass.SWITCH, 21 | entity_registry_enabled_default=True, 22 | value_fn=lambda landroid: landroid.partymode_enabled, 23 | command_fn=lambda landroid, serial, state: landroid.set_partymode( 24 | serial, state 25 | ), 26 | icon="mdi:party-popper", 27 | ), 28 | LandroidSwitchEntityDescription( 29 | key="locked", 30 | name="Locked", 31 | entity_category=EntityCategory.CONFIG, 32 | device_class=SwitchDeviceClass.SWITCH, 33 | entity_registry_enabled_default=False, 34 | value_fn=lambda landroid: landroid.locked, 35 | command_fn=lambda landroid, serial, state: landroid.set_lock(serial, state), 36 | icon_on="mdi:lock", 37 | icon_off="mdi:lock-open", 38 | ), 39 | LandroidSwitchEntityDescription( 40 | key="offlimits", 41 | name="Off limits", 42 | entity_category=EntityCategory.CONFIG, 43 | device_class=SwitchDeviceClass.SWITCH, 44 | entity_registry_enabled_default=True, 45 | value_fn=lambda landroid: landroid.offlimit, 46 | command_fn=lambda landroid, serial, state: landroid.set_offlimits( 47 | serial, state 48 | ), 49 | icon="mdi:border-none-variant", 50 | required_feature=LandroidFeatureSupport.OFFLIMITS, 51 | ), 52 | LandroidSwitchEntityDescription( 53 | key="offlimits_shortcut", 54 | name="Shortcuts", 55 | entity_category=EntityCategory.CONFIG, 56 | device_class=SwitchDeviceClass.SWITCH, 57 | entity_registry_enabled_default=True, 58 | value_fn=lambda landroid: landroid.offlimit_shortcut, 59 | command_fn=lambda landroid, serial, state: landroid.set_offlimits_shortcut( 60 | serial, state 61 | ), 62 | icon="mdi:transit-detour", 63 | required_feature=LandroidFeatureSupport.OFFLIMITS, 64 | ), 65 | LandroidSwitchEntityDescription( 66 | key="acs", 67 | name="ACS", 68 | entity_category=EntityCategory.CONFIG, 69 | device_class=SwitchDeviceClass.SWITCH, 70 | entity_registry_enabled_default=True, 71 | value_fn=lambda landroid: landroid.acs_enabled, 72 | command_fn=lambda landroid, serial, state: landroid.set_acs(serial, state), 73 | icon="mdi:transit-connection-variant", 74 | required_feature=LandroidFeatureSupport.ACS, 75 | ), 76 | ] 77 | 78 | 79 | async def async_setup_entry( 80 | hass: HomeAssistant, 81 | config: ConfigEntry, 82 | async_add_devices, 83 | ) -> None: 84 | """Set up the switch platform.""" 85 | switches = [] 86 | for _, info in hass.data[DOMAIN][config.entry_id][ATTR_DEVICES].items(): 87 | api: LandroidAPI = info["api"] 88 | logger = LandroidLogger(name=__name__, api=api, log_level=LOGLEVEL) 89 | for sens in SWITCHES: 90 | logger.log( 91 | LoggerType.API, 92 | "API features: %s, Required feature: %s", 93 | api.features, 94 | sens.required_feature, 95 | ) 96 | if isinstance(sens.required_feature, type(None)) or ( 97 | api.features & sens.required_feature 98 | ): 99 | entity = LandroidSwitch(hass, sens, api, config) 100 | 101 | switches.append(entity) 102 | 103 | async_add_devices(switches) 104 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.yml: -------------------------------------------------------------------------------- 1 | name: Report a bug / issue 2 | description: Report an issue with the integration - PLEASE KEEP IT IN ENGLISH! 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Note: 9 | This issue form is for reporting bugs only! 10 | KEEP THE LANGUAGE AT ENGLISH PLEASE! 11 | 12 | PLEASE MAKE SURE YOU HAVE INSTALLED THE LATEST VERSION BEFORE SUBMITTING A NEW ISSUE! 13 | - type: textarea 14 | validations: 15 | required: true 16 | attributes: 17 | label: Describe the issue 18 | description: >- 19 | Describe the issue you are experiencing here. 20 | Describe what you were trying to do and what happened. 21 | 22 | Provide a clear and concise description of what the problem is. 23 | - type: markdown 24 | attributes: 25 | value: | 26 | ## Environment 27 | - type: input 28 | id: version 29 | validations: 30 | required: true 31 | attributes: 32 | label: What version of Home Assistant Core has the issue? 33 | placeholder: core- 34 | description: > 35 | Can be found in: [Settings ⇒ System ⇒ Repairs ⇒ Three Dots in Upper Right ⇒ System information](https://my.home-assistant.io/redirect/system_health/). 36 | 37 | [![Open your Home Assistant instance and show the system information.](https://my.home-assistant.io/badges/system_health.svg)](https://my.home-assistant.io/redirect/system_health/) 38 | - type: input 39 | attributes: 40 | label: What was the last working version of Home Assistant Core? 41 | placeholder: core- 42 | description: > 43 | If known, otherwise leave blank. 44 | - type: input 45 | validations: 46 | required: true 47 | attributes: 48 | label: What version of the Landroid Cloud integration do you have installed 49 | - type: dropdown 50 | validations: 51 | required: true 52 | attributes: 53 | label: What type of installation are you running? 54 | description: > 55 | Can be found in: [Settings ⇒ System ⇒ Repairs ⇒ Three Dots in Upper Right ⇒ System information](https://my.home-assistant.io/redirect/system_health/). 56 | 57 | [![Open your Home Assistant instance and show the system information.](https://my.home-assistant.io/badges/system_health.svg)](https://my.home-assistant.io/redirect/system_health/) 58 | options: 59 | - Home Assistant OS 60 | - Home Assistant Container 61 | - Home Assistant Supervised 62 | - Home Assistant Core 63 | - type: input 64 | validations: 65 | required: true 66 | attributes: 67 | label: Which make and model is the mower used for this integration? 68 | placeholder: Worx WRxyz or Landroid M500 69 | - type: markdown 70 | attributes: 71 | value: | 72 | # Details 73 | - type: textarea 74 | validations: 75 | required: true 76 | attributes: 77 | label: Diagnostics information (NOT log entries!) 78 | placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)" 79 | description: >- 80 | This integrations provide the ability to [download diagnostic data](https://www.home-assistant.io/docs/configuration/troubleshooting/#debug-logs-and-diagnostics). 81 | 82 | **It would really help if you could download the diagnostics data for the device you are having issues with, 83 | and drag-and-drop that file into the textbox below.** 84 | 85 | It generally allows pinpointing defects and thus resolving issues faster. 86 | 87 | If you are unable to provide the diagnostics (ie. you cannot add the integration), please write **None** in this field. 88 | - type: textarea 89 | validations: 90 | required: true 91 | attributes: 92 | label: Relevant log entries 93 | description: >- 94 | Anything from home-assistant.log that has any direct relevance for this issue 95 | 96 | If you are unable to provide any relevant log entries, please write **None** in this field. 97 | render: txt 98 | - type: textarea 99 | attributes: 100 | label: Additional information 101 | description: > 102 | If you have any additional information for us, use the field below. 103 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/number.py: -------------------------------------------------------------------------------- 1 | """Input numbers for landroid_cloud.""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | 7 | from homeassistant.components.number import NumberDeviceClass, NumberMode 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.const import EntityCategory 10 | from homeassistant.core import HomeAssistant 11 | from pyworxcloud import DeviceCapability 12 | 13 | from .api import LandroidAPI 14 | from .const import ATTR_DEVICES, DOMAIN 15 | from .device_base import LandroidNumber, LandroidNumberEntityDescription 16 | 17 | INPUT_NUMBERS = [ 18 | LandroidNumberEntityDescription( 19 | key="timeextension", 20 | name="Time extension", 21 | entity_category=EntityCategory.CONFIG, 22 | device_class=None, 23 | entity_registry_enabled_default=True, 24 | native_unit_of_measurement="%", 25 | native_min_value=-100, 26 | native_max_value=100, 27 | native_step=10, 28 | mode=NumberMode.SLIDER, 29 | value_fn=lambda api: api.cloud.devices[api.device_name].schedules[ 30 | "time_extension" 31 | ], 32 | command_fn=lambda api, value: api.cloud.send( 33 | api.device.serial_number, json.dumps({"sc": {"p": value}}) 34 | ), 35 | required_protocol=0, 36 | ), 37 | LandroidNumberEntityDescription( 38 | key="torque", 39 | name="Torque", 40 | entity_category=EntityCategory.CONFIG, 41 | device_class=NumberDeviceClass.POWER_FACTOR, 42 | entity_registry_enabled_default=True, 43 | native_unit_of_measurement=None, 44 | native_min_value=-50, 45 | native_max_value=50, 46 | native_step=1, 47 | mode=NumberMode.SLIDER, 48 | value_fn=lambda api: api.cloud.devices[api.device_name].torque, 49 | command_fn=lambda api, value: api.cloud.send( 50 | api.device.serial_number, json.dumps({"tq": value}) 51 | ), 52 | required_capability=DeviceCapability.TORQUE, 53 | ), 54 | LandroidNumberEntityDescription( 55 | key="raindelay", 56 | name="Raindelay", 57 | entity_category=EntityCategory.CONFIG, 58 | device_class=None, 59 | entity_registry_enabled_default=True, 60 | native_unit_of_measurement="minutes", 61 | native_min_value=0, 62 | native_max_value=300, 63 | native_step=1, 64 | mode=NumberMode.BOX, 65 | value_fn=lambda api: api.device.rainsensor["delay"], 66 | command_fn=lambda api, value: api.cloud.raindelay( 67 | api.device.serial_number, value 68 | ), 69 | icon="mdi:weather-rainy", 70 | ), 71 | LandroidNumberEntityDescription( 72 | key="cutting_height", 73 | name="Cutting height", 74 | entity_category=EntityCategory.CONFIG, 75 | device_class=NumberDeviceClass.DISTANCE, 76 | entity_registry_enabled_default=True, 77 | native_unit_of_measurement="mm", 78 | native_min_value=30, 79 | native_max_value=60, 80 | native_step=5, 81 | mode=NumberMode.SLIDER, 82 | value_fn=lambda api: api.cloud.get_cutting_height(api.device.serial_number), 83 | command_fn=lambda api, value: api.cloud.set_cutting_height( 84 | api.device.serial_number, value 85 | ), 86 | required_capability=DeviceCapability.CUTTING_HEIGHT, 87 | ), 88 | ] 89 | 90 | 91 | async def async_setup_entry( 92 | hass: HomeAssistant, 93 | config: ConfigEntry, 94 | async_add_devices, 95 | ) -> None: 96 | """Set up the switch platform.""" 97 | entities = [] 98 | for _, info in hass.data[DOMAIN][config.entry_id][ATTR_DEVICES].items(): 99 | api: LandroidAPI = info["api"] 100 | for number in INPUT_NUMBERS: 101 | if ( 102 | isinstance(number.required_protocol, type(None)) 103 | or number.required_protocol == api.device.protocol 104 | ) and ( 105 | isinstance(number.required_capability, type(None)) 106 | or api.device.capabilities.check(number.required_capability) 107 | ): 108 | entity = LandroidNumber(hass, number, api, config) 109 | entities.append(entity) 110 | 111 | async_add_devices(entities) 112 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/utils/logger.py: -------------------------------------------------------------------------------- 1 | """Landroid Cloud integration wide logger component.""" 2 | 3 | # pylint: disable=unused-argument,relative-beyond-top-level 4 | from __future__ import annotations 5 | 6 | import contextlib 7 | import enum 8 | import logging 9 | 10 | try: 11 | from ..api import LandroidAPI 12 | except: # pylint: disable=bare-except 13 | contextlib.suppress(Exception) # pass 14 | 15 | 16 | class LoggerType(enum.StrEnum): 17 | """Defines the available logger types.""" 18 | 19 | NONE = "None" 20 | API = "API" 21 | GENERIC = "Generic" 22 | AUTHENTICATION = "Authentication" 23 | DATA_UPDATE = "Update signal" 24 | SETUP = "Setup" 25 | SETUP_IMPORT = "Setup, Import" 26 | CONFIG = "Config" 27 | CONFIG_IMPORT = "Config, Import" 28 | SERVICE = "Service" 29 | SERVICE_REGISTER = "Service Register" 30 | SERVICE_ADD = "Service Add" 31 | SERVICE_CALL = "Service Call" 32 | FEATURE = "Feature" 33 | FEATURE_ASSESSMENT = "Feature Assessment" 34 | BUTTON = "Button" 35 | SELECT = "Select" 36 | MOWER = "Mower" 37 | SENSOR = "Sensor" 38 | DEVELOP = "DEVELOPER INFO" 39 | 40 | 41 | class LogLevel(enum.StrEnum): 42 | """Define loglevels.""" 43 | 44 | CRITICAL = "critical" 45 | ERROR = "error" 46 | WARNING = "warning" 47 | INFO = "info" 48 | DEBUG = "debug" 49 | 50 | 51 | class LandroidLogger: 52 | """Basic logger instance.""" 53 | 54 | def __init__( 55 | self, 56 | name: str = None, 57 | api: LandroidAPI = None, 58 | log_level: LogLevel = LogLevel.DEBUG, 59 | ) -> None: 60 | """Initialize base logger.""" 61 | 62 | self.logapi = api 63 | self.logname = name 64 | self.loglevel = log_level 65 | self.logdevicename = None 66 | 67 | if self.logapi: 68 | if hasattr(self.logapi, "friendly_name"): 69 | self.logdevicename = self.logapi.friendly_name 70 | elif hasattr(self.logapi, "name"): 71 | self.logdevicename = self.logapi.name 72 | 73 | def log( 74 | self, 75 | log_type: LoggerType | None, 76 | message: str, 77 | *args, 78 | log_level: str | None = None, 79 | device: str | bool | None = False, 80 | ): 81 | """Write to logger component.""" 82 | logger = logging.getLogger(self.logname) 83 | 84 | prefix = "" 85 | if log_type not in [LoggerType.NONE, None]: 86 | if not device and not isinstance(device, type(None)): 87 | prefix = ( 88 | "(" + log_type + ") " 89 | if isinstance(self.logapi, type(None)) 90 | else "(" 91 | + ( 92 | self.logapi.friendly_name 93 | if hasattr(self.logapi, "friendly_name") 94 | else self.logapi.name 95 | ) 96 | + ", " 97 | + log_type 98 | + ") " 99 | ) 100 | else: 101 | prefix = ( 102 | "(" + log_type + ") " 103 | if isinstance(device, type(None)) 104 | else "(" + device + ", " + log_type + ") " 105 | ) 106 | 107 | log_string = prefix + str(message) 108 | level = self.loglevel if isinstance(log_level, type(None)) else log_level 109 | 110 | if level == "info": 111 | if args: 112 | logger.info(log_string, *args) 113 | else: 114 | logger.info(log_string) 115 | elif level == "warning": 116 | if args: 117 | logger.warning(log_string, *args) 118 | else: 119 | logger.warning(log_string) 120 | elif level == "critical": 121 | if args: 122 | logger.critical(log_string, *args) 123 | else: 124 | logger.critical(log_string) 125 | elif level == "error": 126 | if args: 127 | logger.error(log_string, *args) 128 | else: 129 | logger.error(log_string) 130 | elif level == "debug": 131 | logger.debug(log_string, *args) 132 | 133 | def log_set_api(self, api: LandroidAPI) -> None: 134 | """Set integration API.""" 135 | self.logapi = api 136 | self.logdevicename = api.friendly_name 137 | 138 | def log_set_name(self, name: str) -> None: 139 | """Set the namespace name used in logging.""" 140 | self.logname = name 141 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/services.py: -------------------------------------------------------------------------------- 1 | """Services definitions.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | 7 | from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID 8 | from homeassistant.core import HomeAssistant, ServiceCall, callback 9 | from homeassistant.exceptions import HomeAssistantError 10 | from homeassistant.helpers import device_registry as dr 11 | from homeassistant.helpers import entity_registry as er 12 | from homeassistant.helpers.device_registry import DeviceEntry 13 | 14 | from .api import LandroidAPI 15 | from .const import ( 16 | ATTR_API, 17 | ATTR_DEVICEIDS, 18 | ATTR_DEVICES, 19 | ATTR_SERVICE, 20 | DOMAIN, 21 | LOGLEVEL, 22 | SERVICE_CONFIG, 23 | SERVICE_OTS, 24 | SERVICE_SCHEDULE, 25 | SERVICE_SEND_RAW, 26 | LandroidFeatureSupport, 27 | ) 28 | from .scheme import CONFIG_SCHEMA, EMPTY_SCHEME, OTS_SCHEME, RAW_SCHEME, SCHEDULE_SCHEME 29 | from .utils.logger import LandroidLogger, LoggerType 30 | 31 | 32 | @dataclass 33 | class LandroidServiceDescription: 34 | """A class that describes Home Assistant entities.""" 35 | 36 | # This is the key identifier for this entity 37 | key: str 38 | feature: LandroidFeatureSupport | None = None 39 | schema: str = EMPTY_SCHEME 40 | 41 | 42 | SUPPORTED_SERVICES = [ 43 | LandroidServiceDescription( 44 | key=SERVICE_CONFIG, schema=CONFIG_SCHEMA, feature=LandroidFeatureSupport.CONFIG 45 | ), 46 | LandroidServiceDescription( 47 | key=SERVICE_OTS, schema=OTS_SCHEME, feature=LandroidFeatureSupport.OTS 48 | ), 49 | LandroidServiceDescription( 50 | key=SERVICE_SCHEDULE, 51 | schema=SCHEDULE_SCHEME, 52 | feature=LandroidFeatureSupport.SCHEDULES, 53 | ), 54 | LandroidServiceDescription( 55 | key=SERVICE_SEND_RAW, feature=LandroidFeatureSupport.CONFIG, schema=RAW_SCHEME 56 | ), 57 | ] 58 | 59 | 60 | @callback 61 | async def async_setup_services(hass: HomeAssistant) -> None: 62 | """Set up services for Landroid Cloud integration.""" 63 | 64 | async def async_call_landroid_service(service_call: ServiceCall) -> None: 65 | """Call correct Landroid Cloud service.""" 66 | service = service_call.service 67 | service_data = service_call.data 68 | 69 | device_registry = dr.async_get(hass) 70 | entity_registry = er.async_get(hass) 71 | 72 | devices: DeviceEntry = [] 73 | 74 | if CONF_DEVICE_ID in service_data: 75 | if isinstance(service_data[CONF_DEVICE_ID], str): 76 | devices.append(device_registry.async_get(service_data[CONF_DEVICE_ID])) 77 | else: 78 | for entry in service_data[CONF_DEVICE_ID]: 79 | devices.append(device_registry.async_get(entry)) 80 | else: 81 | for entry in service_data[CONF_ENTITY_ID]: 82 | devices.append( 83 | device_registry.async_get( 84 | entity_registry.entities.get(entry).device_id 85 | ) 86 | ) 87 | 88 | for device in devices: 89 | api: LandroidAPI = await async_match_api(hass, device) 90 | 91 | if isinstance(api, type(None)): 92 | raise HomeAssistantError( 93 | f"Failed to call service '{service_call.service}'. Config entry for target not found" 94 | ) 95 | 96 | if service not in api.services: 97 | raise HomeAssistantError( 98 | f"Failed to call service '{service_call.service}'. " 99 | "Service is not supported by this device." 100 | ) 101 | 102 | if not api.device.online: 103 | raise HomeAssistantError( 104 | f"Failed to call service '{service_call.service}'. " 105 | "Device is currently offline." 106 | ) 107 | 108 | await api.services[service][ATTR_SERVICE](service_data) 109 | 110 | logger = LandroidLogger(name=__name__, log_level=LOGLEVEL) 111 | for service in SUPPORTED_SERVICES: 112 | if not hass.services.has_service(DOMAIN, service.key): 113 | logger.log(LoggerType.SERVICE_ADD, "Adding %s", service.key) 114 | hass.services.async_register( 115 | DOMAIN, 116 | service.key, 117 | async_call_landroid_service, 118 | schema=service.schema, 119 | ) 120 | 121 | 122 | async def async_match_api( 123 | hass: HomeAssistant, device: DeviceEntry 124 | ) -> LandroidAPI | None: 125 | """Match device to API.""" 126 | logger = LandroidLogger(name=__name__, log_level=LOGLEVEL) 127 | if not hasattr(device, "id"): 128 | raise HomeAssistantError("No valid device object was specified.") 129 | 130 | logger.log(LoggerType.SERVICE_CALL, "Trying to match ID '%s'", device.id) 131 | for possible_entry in hass.data[DOMAIN].values(): 132 | if ATTR_DEVICEIDS not in possible_entry: 133 | continue 134 | device_ids = possible_entry[ATTR_DEVICEIDS] 135 | logger.log( 136 | LoggerType.SERVICE_CALL, "Checking for '%s' in %s", device.id, device_ids 137 | ) 138 | for name, did in device_ids.items(): 139 | logger.log(LoggerType.SERVICE_CALL, "Matching '%s' to '%s'", device.id, did) 140 | if did == device.id: 141 | logger.log( 142 | LoggerType.SERVICE_CALL, 143 | "Found a match for '%s' in '%s'", 144 | device.id, 145 | name, 146 | ) 147 | return possible_entry[ATTR_DEVICES][name][ATTR_API] 148 | 149 | return None 150 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/config_flow.py: -------------------------------------------------------------------------------- 1 | """Adds support for Landroid Cloud compatible devices.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant import config_entries, core, exceptions 6 | from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TYPE 7 | from pyworxcloud import WorxCloud 8 | from pyworxcloud.exceptions import AuthorizationError, TooManyRequestsError 9 | 10 | from .const import DOMAIN, LOGLEVEL 11 | from .scheme import DATA_SCHEMA 12 | from .utils.logger import LandroidLogger, LoggerType, LogLevel 13 | 14 | LOGGER = LandroidLogger(name=__name__, log_level=LOGLEVEL) 15 | 16 | 17 | async def validate_input(hass: core.HomeAssistant, data): 18 | """Validate the user input allows us to connect. 19 | 20 | Data has the keys from DATA_SCHEMA with values provided by the user. 21 | """ 22 | 23 | LOGGER.log(LoggerType.CONFIG, "data: %s", data) 24 | 25 | worx = WorxCloud( 26 | data.get(CONF_EMAIL), data.get(CONF_PASSWORD), data.get(CONF_TYPE).lower() 27 | ) 28 | try: 29 | auth = await hass.async_add_executor_job(worx.authenticate) 30 | except TooManyRequestsError: 31 | raise TooManyRequests from None 32 | except AuthorizationError: 33 | raise InvalidAuth from None 34 | 35 | if not auth: 36 | raise InvalidAuth 37 | 38 | return {"title": f"{data[CONF_TYPE]} - {data[CONF_EMAIL]}"} 39 | 40 | 41 | class InvalidAuth(exceptions.HomeAssistantError): 42 | """Error to indicate there is invalid auth.""" 43 | 44 | 45 | class CannotConnect(exceptions.HomeAssistantError): 46 | """Error to indicate we cannot connect.""" 47 | 48 | 49 | class TooManyRequests(exceptions.HomeAssistantError): 50 | """Error to indicate we made too many requests.""" 51 | 52 | 53 | class LandroidCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 54 | """Handle a config flow for Landroid Cloud.""" 55 | 56 | VERSION = 1 57 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH 58 | 59 | def check_for_existing(self, data): 60 | """Check whether an existing entry is using the same URLs.""" 61 | return any( 62 | entry.data.get(CONF_EMAIL) == data.get(CONF_EMAIL) 63 | and entry.data.get(CONF_TYPE).lower() 64 | == ( 65 | data.get(CONF_TYPE).lower() 66 | if not isinstance(data.get(CONF_TYPE), type(None)) 67 | else "worx" 68 | ) 69 | for entry in self._async_current_entries() 70 | ) 71 | 72 | def __init__(self): 73 | """Initialize the config flow.""" 74 | self._errors = {} 75 | 76 | async def async_step_user(self, user_input=None): 77 | """Handle the initial Landroid Cloud step.""" 78 | self._errors = {} 79 | if user_input is not None: 80 | if self.check_for_existing(user_input): 81 | return self.async_abort(reason="already_exists") 82 | 83 | try: 84 | validated = await validate_input(self.hass, user_input) 85 | except CannotConnect: 86 | self._errors["base"] = "cannot_connect" 87 | except InvalidAuth: 88 | self._errors["base"] = "invalid_auth" 89 | except TooManyRequests: 90 | self._errors["base"] = "too_many_requests" 91 | except Exception as ex: # pylint: disable=broad-except 92 | LOGGER.log( 93 | LoggerType.CONFIG, 94 | "Unexpected exception: %s", 95 | ex, 96 | log_level=LogLevel.ERROR, 97 | ) 98 | self._errors["base"] = "unknown" 99 | 100 | if "base" not in self._errors: 101 | await self.async_set_unique_id( 102 | f"{user_input[CONF_EMAIL]}_{user_input[CONF_TYPE]}" 103 | ) 104 | 105 | return self.async_create_entry( 106 | title=validated["title"], 107 | data=user_input, 108 | description=f"API connector for {validated['title']} cloud", 109 | ) 110 | 111 | return self.async_show_form( 112 | step_id="user", data_schema=DATA_SCHEMA, errors=self._errors 113 | ) 114 | 115 | async def async_step_import(self, import_config): 116 | """Import a config entry.""" 117 | if import_config is not None: 118 | if self.check_for_existing(import_config): 119 | LOGGER.log( 120 | LoggerType.CONFIG_IMPORT, 121 | "Landroid_cloud configuration for %s already imported, you can " 122 | "safely remove the entry from your configuration.yaml as this " 123 | "is no longer used", 124 | import_config.get(CONF_EMAIL), 125 | log_level=LogLevel.WARNING, 126 | ) 127 | return self.async_abort(reason="already_exists") 128 | 129 | try: 130 | await validate_input(self.hass, import_config) 131 | except CannotConnect: 132 | self._errors["base"] = "cannot_connect" 133 | except InvalidAuth: 134 | self._errors["base"] = "invalid_auth" 135 | except Exception: # pylint: disable=broad-except 136 | LOGGER.log( 137 | LoggerType.CONFIG_IMPORT, 138 | "Unexpected exception", 139 | log_level=LogLevel.ERROR, 140 | ) 141 | self._errors["base"] = "unknown" 142 | 143 | if "base" not in self._errors: 144 | return self.async_create_entry( 145 | title=f"Import - {import_config.get(CONF_EMAIL)}", 146 | data=import_config, 147 | ) 148 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/services.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | description: Set device config parameters 3 | target: 4 | entity: 5 | integration: landroid_cloud 6 | domain: lawn_mower 7 | fields: 8 | multizone_distances: 9 | name: Multi zone distances 10 | description: 'Set multizone distance array in meters. 0 = Disabled. Format: 15, 80, 120, 155' 11 | example: '15, 80, 120, 155' 12 | selector: 13 | text: 14 | multizone_probabilities: 15 | name: Multi zone probabilities 16 | description: 'Set multizone probabilities array. Format: 50, 10, 20, 20' 17 | example: '50, 10, 20, 20' 18 | selector: 19 | text: 20 | 21 | ots: 22 | description: Start One-Time-Schedule (if supported) 23 | target: 24 | entity: 25 | integration: landroid_cloud 26 | domain: lawn_mower 27 | fields: 28 | boundary: 29 | name: Boundary 30 | description: Do boundary (Edge cut) 31 | example: true 32 | required: true 33 | default: false 34 | selector: 35 | boolean: 36 | runtime: 37 | name: Run time 38 | description: Run time in minutes 39 | example: 60 40 | required: true 41 | default: 30 42 | selector: 43 | number: 44 | min: 10 45 | max: 120 46 | step: 1 47 | unit_of_measurement: "minutes" 48 | mode: slider 49 | 50 | schedule: 51 | description: Set or change the schedule of the mower 52 | target: 53 | entity: 54 | integration: landroid_cloud 55 | domain: lawn_mower 56 | fields: 57 | type: 58 | name: Schedule type 59 | description: Change primary or secondary schedule? 60 | example: primary 61 | required: true 62 | default: "primary" 63 | selector: 64 | select: 65 | options: 66 | - "primary" 67 | - "secondary" 68 | monday_start: 69 | name: Monday, Start 70 | description: Starting time for monday 71 | example: "11:00" 72 | selector: 73 | time: 74 | monday_end: 75 | name: Monday, End 76 | description: When should the schedule stop on mondays? 77 | example: "16:00" 78 | selector: 79 | time: 80 | monday_boundary: 81 | name: Monday, Boundary 82 | description: Should we start this schedule by cutting the boundary (edge cut)? 83 | example: false 84 | selector: 85 | boolean: 86 | tuesday_start: 87 | name: Tuesday, Start 88 | description: When should the device start the task? 89 | example: "11:00" 90 | selector: 91 | time: 92 | tuesday_end: 93 | name: Tuesday, End 94 | description: When should the task stop? 95 | example: "16:00" 96 | selector: 97 | time: 98 | tuesday_boundary: 99 | name: Tuesday, Boundary 100 | description: Should we start this task by cutting the boundary (edge cut)? 101 | example: false 102 | selector: 103 | boolean: 104 | wednesday_start: 105 | name: Wednesday, Start 106 | description: Starting time for monday 107 | example: "11:00" 108 | selector: 109 | time: 110 | wednesday_end: 111 | name: Wednesday, End 112 | description: When should the schedule stop on mondays? 113 | example: "16:00" 114 | selector: 115 | time: 116 | wednesday_boundary: 117 | name: Wednesday, Boundary 118 | description: Should we start this schedule by cutting the boundary (edge cut)? 119 | example: false 120 | selector: 121 | boolean: 122 | thursday_start: 123 | name: Thursday, Start 124 | description: When should the device start the task? 125 | example: "11:00" 126 | selector: 127 | time: 128 | thursday_end: 129 | name: Thursday, End 130 | description: When should the task stop? 131 | example: "16:00" 132 | selector: 133 | time: 134 | thursday_boundary: 135 | name: Thursday, Boundary 136 | description: Should we start this task by cutting the boundary (edge cut)? 137 | example: false 138 | selector: 139 | boolean: 140 | friday_start: 141 | name: Friday, Start 142 | description: When should the device start the task? 143 | example: "11:00" 144 | selector: 145 | time: 146 | friday_end: 147 | name: Friday, End 148 | description: When should the task stop? 149 | example: "16:00" 150 | selector: 151 | time: 152 | friday_boundary: 153 | name: Friday, Boundary 154 | description: Should we start this task by cutting the boundary (edge cut)? 155 | example: false 156 | selector: 157 | boolean: 158 | saturday_start: 159 | name: Saturday, Start 160 | description: Starting time for monday 161 | example: "11:00" 162 | selector: 163 | time: 164 | saturday_end: 165 | name: Saturday, End 166 | description: When should the schedule stop on mondays? 167 | example: "16:00" 168 | selector: 169 | time: 170 | saturday_boundary: 171 | name: Saturday, Boundary 172 | description: Should we start this schedule by cutting the boundary (edge cut)? 173 | example: false 174 | selector: 175 | boolean: 176 | sunday_start: 177 | name: Sunday, Start 178 | description: When should the device start the task? 179 | example: "11:00" 180 | selector: 181 | time: 182 | sunday_end: 183 | name: Sunday, End 184 | description: When should the task stop? 185 | example: "16:00" 186 | selector: 187 | time: 188 | sunday_boundary: 189 | name: Sunday, Boundary 190 | description: Should we start this task by cutting the boundary (edge cut)? 191 | example: false 192 | selector: 193 | boolean: 194 | 195 | send_raw: 196 | description: Send a raw JSON command to the device 197 | target: 198 | entity: 199 | integration: landroid_cloud 200 | domain: lawn_mower 201 | fields: 202 | json: 203 | name: JSON data 204 | description: Data to send, formatted as valid JSON 205 | example: "{'cmd': 1}" 206 | required: true 207 | selector: 208 | text: 209 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/api.py: -------------------------------------------------------------------------------- 1 | """Representing the Landroid Cloud API interface.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | from datetime import datetime, timedelta 7 | from typing import Any 8 | 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TYPE 11 | from homeassistant.core import HomeAssistant, callback 12 | from homeassistant.helpers.dispatcher import dispatcher_send 13 | from homeassistant.util import slugify as util_slugify 14 | from pyworxcloud import WorxCloud 15 | from pyworxcloud.events import LandroidEvent 16 | from pyworxcloud.utils import DeviceCapability, DeviceHandler 17 | 18 | from .const import ( 19 | API_TO_INTEGRATION_FEATURE_MAP, 20 | ATTR_API, 21 | ATTR_CLOUD, 22 | ATTR_DEVICE, 23 | ATTR_DEVICES, 24 | DOMAIN, 25 | UPDATE_SIGNAL, 26 | LandroidFeatureSupport, 27 | ) 28 | from .utils.logger import LandroidLogger, LoggerType 29 | 30 | 31 | class LandroidAPI: 32 | """Handle the API calls.""" 33 | 34 | def __init__(self, hass: HomeAssistant, device_name: str, entry: ConfigEntry): 35 | """Initialize API connection for a device. 36 | 37 | Args: 38 | hass (HomeAssistant): Home Assistant object 39 | device_name (str): Name of the mower device. 40 | entry (ConfigEntry): Home Assistant configuration entry for the cloud account. 41 | 42 | """ 43 | self.hass = hass 44 | self.entry_id = entry.entry_id 45 | self.data = entry.data 46 | self.options = entry.options 47 | self.entry = entry 48 | self.cloud: WorxCloud = hass.data[DOMAIN][entry.entry_id][ATTR_CLOUD] 49 | self.device: DeviceHandler = self.cloud.devices[device_name] 50 | self.unique_id = entry.unique_id 51 | self.services = {} 52 | self.shared_options = {} 53 | self.device_id = None 54 | self.features = 0 55 | self.features_loaded = False 56 | 57 | self.device_name = device_name 58 | 59 | self.name = util_slugify(f"{device_name}") 60 | self.friendly_name = device_name 61 | 62 | self.config = { 63 | "email": hass.data[DOMAIN][entry.entry_id][CONF_EMAIL].lower(), 64 | "password": hass.data[DOMAIN][entry.entry_id][CONF_PASSWORD], 65 | "type": hass.data[DOMAIN][entry.entry_id][CONF_TYPE].lower(), 66 | } 67 | 68 | self.logger = LandroidLogger(name=__name__, api=self) 69 | self.cloud.set_callback(LandroidEvent.DATA_RECEIVED, self.receive_data) 70 | self.cloud.set_callback(LandroidEvent.API, self.receive_data) 71 | 72 | self.logger.log(LoggerType.API, "Device: %s", vars(self.device)) 73 | 74 | async def async_await_features(self, timeout: int = 30) -> None: 75 | """Await feature checks.""" 76 | timeout_at = datetime.now() + timedelta(seconds=timeout) 77 | 78 | while not self.features_loaded: 79 | if datetime.now() > timeout_at: 80 | break 81 | 82 | if ( 83 | not self.device.capabilities.ready 84 | or not self.features_loaded 85 | or self.features == 0 86 | ): 87 | raise ValueError( 88 | f"Capabilities ready: {self.device.capabilities.ready} -- Features loaded: {self.features_loaded} -- Feature bits: {self.features}" 89 | ) 90 | 91 | self.device.mqtt.set_eventloop(self.hass.loop) 92 | 93 | def check_features( 94 | self, features: int | None = None, callback_func: Any = None 95 | ) -> None: 96 | """Check which features the device supports. 97 | 98 | Args: 99 | features (int): Current feature set. 100 | callback_func (_type_, optional): 101 | Function to be called when the features 102 | have been assessed. Defaults to None. 103 | 104 | """ 105 | self.logger.log(LoggerType.FEATURE_ASSESSMENT, "Assessing available features") 106 | if isinstance(features, type(None)): 107 | features = self.features 108 | 109 | if self.has_feature(DeviceCapability.PARTY_MODE): 110 | self.logger.log(LoggerType.FEATURE_ASSESSMENT, "Party mode capable") 111 | features = features | LandroidFeatureSupport.PARTYMODE 112 | 113 | if self.has_feature(DeviceCapability.ONE_TIME_SCHEDULE): 114 | self.logger.log(LoggerType.FEATURE_ASSESSMENT, "OTS capable") 115 | features = features | LandroidFeatureSupport.OTS 116 | 117 | if self.has_feature(DeviceCapability.EDGE_CUT): 118 | self.logger.log(LoggerType.FEATURE_ASSESSMENT, "Edge Cut capable") 119 | features = features | LandroidFeatureSupport.EDGECUT 120 | 121 | if self.has_feature(DeviceCapability.TORQUE): 122 | self.logger.log(LoggerType.FEATURE_ASSESSMENT, "Torque capable") 123 | features = features | LandroidFeatureSupport.TORQUE 124 | 125 | if self.has_feature(DeviceCapability.OFF_LIMITS): 126 | self.logger.log( 127 | LoggerType.FEATURE_ASSESSMENT, "Off limits module available" 128 | ) 129 | features = features | LandroidFeatureSupport.OFFLIMITS 130 | 131 | if self.has_feature(DeviceCapability.ACS): 132 | self.logger.log(LoggerType.FEATURE_ASSESSMENT, "ACS module available") 133 | features = features | LandroidFeatureSupport.ACS 134 | 135 | old_feature = self.features 136 | self.features = features 137 | 138 | if callback_func: 139 | callback_func(old_feature) 140 | 141 | def has_feature(self, api_feature: DeviceCapability) -> bool: 142 | """Check if the feature is already known. 143 | 144 | Return True if feature is supported and not known to us. 145 | Returns False if not supported or already known. 146 | """ 147 | 148 | if API_TO_INTEGRATION_FEATURE_MAP[api_feature] & self.features != 0: 149 | return False 150 | else: 151 | return self.device.capabilities.check(api_feature) 152 | 153 | @callback 154 | def receive_data( 155 | self, name: str, device: DeviceHandler # pylint: disable=unused-argument 156 | ) -> None: 157 | """Handle data when the MQTT broker sends new data or API is updated.""" 158 | self.logger.log( 159 | LoggerType.DATA_UPDATE, 160 | "Received new data for %s", 161 | name, 162 | device=name, 163 | ) 164 | 165 | dispatcher_send(self.hass, util_slugify(f"{UPDATE_SIGNAL}_{name}")) 166 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/__init__.py: -------------------------------------------------------------------------------- 1 | """Adds support for Landroid Cloud compatible devices.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | 7 | import requests 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TYPE 10 | from homeassistant.core import HomeAssistant 11 | from homeassistant.exceptions import ConfigEntryNotReady 12 | from homeassistant.loader import async_get_integration 13 | from pyworxcloud import WorxCloud, exceptions 14 | 15 | from .api import LandroidAPI 16 | from .const import ( 17 | ATTR_API, 18 | ATTR_CLOUD, 19 | ATTR_DEVICE, 20 | ATTR_DEVICEIDS, 21 | ATTR_DEVICES, 22 | ATTR_FEATUREBITS, 23 | DOMAIN, 24 | LOGLEVEL, 25 | PLATFORMS_PRIMARY, 26 | PLATFORMS_SECONDARY, 27 | STARTUP, 28 | ) 29 | from .services import async_setup_services 30 | from .utils.logger import LandroidLogger, LoggerType, LogLevel 31 | 32 | LOGGER = LandroidLogger(name=__name__, log_level=LOGLEVEL) 33 | 34 | 35 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 36 | """Set up cloud API connector from a config entry.""" 37 | hass.data.setdefault(DOMAIN, {}) 38 | 39 | await check_unique_id(hass, entry) 40 | result = await _async_setup(hass, entry) 41 | 42 | if result: 43 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_PRIMARY) 44 | 45 | await async_setup_services(hass) 46 | 47 | return result 48 | 49 | 50 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 51 | """Unload a config entry.""" 52 | unload_ok = await hass.config_entries.async_unload_platforms( 53 | entry, PLATFORMS_PRIMARY + PLATFORMS_SECONDARY 54 | ) 55 | 56 | services = [] 57 | if unload_ok: 58 | await hass.async_add_executor_job( 59 | hass.data[DOMAIN][entry.entry_id][ATTR_CLOUD].disconnect 60 | ) 61 | 62 | hass.data[DOMAIN].pop(entry.entry_id) 63 | 64 | if not hass.data[DOMAIN]: 65 | for service in services: 66 | hass.services.async_remove(DOMAIN, service) 67 | 68 | return True 69 | 70 | return False 71 | 72 | 73 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 74 | """Reload config entry.""" 75 | await async_unload_entry(hass, entry) 76 | await async_setup_entry(hass, entry) 77 | 78 | 79 | async def _async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool: 80 | """Handle setup of the integration, using a config entry.""" 81 | integration = await async_get_integration(hass, DOMAIN) 82 | LOGGER.log( 83 | None, 84 | STARTUP, 85 | integration.version, 86 | log_level=LogLevel.INFO, 87 | ) 88 | 89 | cloud_email = entry.data.get(CONF_EMAIL) 90 | cloud_password = entry.data.get(CONF_PASSWORD) 91 | cloud_type = entry.data.get(CONF_TYPE) 92 | 93 | if cloud_type is None: 94 | cloud_type = "worx" 95 | 96 | LOGGER.log( 97 | LoggerType.SETUP, 98 | "Opening connection to %s account for %s", 99 | cloud_type, 100 | cloud_email, 101 | ) 102 | cloud = WorxCloud( 103 | cloud_email, cloud_password, cloud_type.lower(), tz=hass.config.time_zone 104 | ) 105 | auth = False 106 | 107 | try: 108 | auth = await hass.async_add_executor_job(cloud.authenticate) 109 | except exceptions.RequestError: 110 | raise ConfigEntryNotReady(f"Request for {cloud_email} was malformed.") 111 | except exceptions.AuthorizationError: 112 | raise ConfigEntryNotReady("Unauthorized - please check your credentials") 113 | except exceptions.ForbiddenError: 114 | raise ConfigEntryNotReady(f"Server rejected access for {cloud_email}") 115 | except exceptions.NotFoundError: 116 | raise ConfigEntryNotReady("No API endpoint was found") 117 | except exceptions.TooManyRequestsError: 118 | raise ConfigEntryNotReady( 119 | f"Too many requests for {cloud_email} - IP address temporary banned." 120 | ) 121 | except exceptions.InternalServerError: 122 | raise ConfigEntryNotReady( 123 | f"Internal server error happend for the request to {cloud_email}" 124 | ) 125 | except exceptions.ServiceUnavailableError: 126 | raise ConfigEntryNotReady("Cloud service unavailable") 127 | except exceptions.APIException as ex: 128 | raise ConfigEntryNotReady("Error connecting to the API") from ex 129 | 130 | if not auth: 131 | raise ConfigEntryNotReady(f"Authentication error for {cloud_email}") 132 | 133 | try: 134 | async with asyncio.timeout(15): 135 | await hass.async_add_executor_job(cloud.connect) 136 | except TimeoutError: 137 | try: 138 | await hass.async_add_executor_job(cloud.disconnect) 139 | raise ConfigEntryNotReady(f"Timed out connecting to account {cloud_email}") 140 | except asyncio.exceptions.CancelledError: 141 | return True 142 | except ConnectionError: 143 | await hass.async_add_executor_job(cloud.disconnect) 144 | raise ConfigEntryNotReady( 145 | f"Connection error connecting to account {cloud_email}" 146 | ) 147 | except requests.exceptions.ConnectionError as err: 148 | LOGGER.log( 149 | LoggerType.API, 150 | "Error connecting to cloud API endpoint - retrying later", 151 | log_level=LogLevel.ERROR, 152 | ) 153 | await hass.async_add_executor_job(cloud.disconnect) 154 | raise ConfigEntryNotReady( 155 | f"Connection error connecting to cloud API endpoint" 156 | ) from err 157 | 158 | hass.data[DOMAIN][entry.entry_id] = { 159 | ATTR_CLOUD: cloud, 160 | ATTR_DEVICES: {}, 161 | ATTR_DEVICEIDS: {}, 162 | ATTR_FEATUREBITS: {}, 163 | CONF_EMAIL: cloud_email, 164 | CONF_PASSWORD: cloud_password, 165 | CONF_TYPE: cloud_type, 166 | } 167 | 168 | for name, device in cloud.devices.items(): 169 | await async_init_device(hass, entry, name, device) 170 | 171 | return True 172 | 173 | 174 | async def async_init_device(hass, entry, name, device) -> None: 175 | """Initialize a device.""" 176 | LOGGER.log( 177 | LoggerType.SETUP, 178 | "Setting up device '%s' on account '%s'", 179 | name, 180 | hass.data[DOMAIN][entry.entry_id][CONF_EMAIL], 181 | ) 182 | api = LandroidAPI(hass, name, entry) 183 | hass.data[DOMAIN][entry.entry_id][ATTR_DEVICEIDS].update({name: None}) 184 | hass.data[DOMAIN][entry.entry_id][ATTR_DEVICES].update( 185 | {name: {ATTR_API: api, ATTR_DEVICE: device}} 186 | ) 187 | 188 | 189 | async def check_unique_id(hass: HomeAssistant, entry: ConfigEntry) -> None: 190 | """Check if a device unique ID is set.""" 191 | if not isinstance(entry.unique_id, type(None)): 192 | return 193 | 194 | new_unique_id = f"{entry.data.get(CONF_EMAIL)}_{entry.data.get(CONF_TYPE)}" 195 | 196 | data = { 197 | CONF_EMAIL: entry.data[CONF_EMAIL], 198 | CONF_PASSWORD: entry.data[CONF_PASSWORD], 199 | CONF_TYPE: entry.data[CONF_TYPE], 200 | } 201 | 202 | hass.config_entries.async_update_entry(entry, data=data, unique_id=new_unique_id) 203 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Ce compte d'utilisateur a déjà été configuré" 5 | }, 6 | "error": { 7 | "cannot_connect": "Échec de la connexion", 8 | "invalid_auth": "Erreur d'authentification", 9 | "unknown": "Erreur inattendue" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "email": "Email", 15 | "password": "Mot de passe", 16 | "type": "Faire" 17 | }, 18 | "title": "Connectez-vous à votre compte Landroid Cloud" 19 | } 20 | } 21 | }, 22 | "entity": { 23 | "lawn_mower": { 24 | "landroid_cloud": { 25 | "state": { 26 | "edgecut": "Bordure de coupe", 27 | "initializing": "Initialisation", 28 | "locked": "Verrouillé", 29 | "mowing": "Tonte", 30 | "offline": "Hors ligne", 31 | "rain_delay": "Retard de pluie", 32 | "returning": "Retour au chargeur", 33 | "searching_zone": "Recherche de zone", 34 | "starting": "Démarrage", 35 | "unknown": "Inconnu", 36 | "zoning": "Formation en zone" 37 | } 38 | } 39 | } 40 | }, 41 | "services": { 42 | "config": { 43 | "description": "Définir les paramètres de configuration de l'appareil", 44 | "fields": { 45 | "multizone_distances": { 46 | "description": "Définissez le tableau de distance multi zone en mètres. 0 = Désactivé. Formats: 15, 80, 120, 155", 47 | "name": "Distances multi zones" 48 | }, 49 | "multizone_probabilities": { 50 | "description": "Définir le tableau de probabilités multi zones. Format: 50, 10, 20, 20", 51 | "name": "Probabilités multi zones" 52 | }, 53 | "raindelay": { 54 | "description": "Réglez le retard de pluie. Temps en minutes allant de 0 à 300. 0 = Désactivé", 55 | "name": "Retard de pluie" 56 | }, 57 | "timeextension": { 58 | "description": "Définir l'augmentation ou la diminution du temps de travail. S'exprime en % allant de -100 à 100", 59 | "name": "Augmentation/diminution du temps de travail" 60 | } 61 | }, 62 | "name": "Définir la zone" 63 | }, 64 | "edgecut": { 65 | "description": "Démarrer la routine de bord (si pris en charge)", 66 | "name": "Routine de bord" 67 | }, 68 | "ots": { 69 | "description": "Démarrer un programme unique (si pris en charge)", 70 | "fields": { 71 | "boundary": { 72 | "description": "Faire le bord (routine de bord)", 73 | "name": "Bord" 74 | }, 75 | "runtime": { 76 | "description": "Durée de fonctionnement en minutes avant de retourner à la station de recharge", 77 | "name": "Durée de fonctionnement" 78 | } 79 | }, 80 | "name": "Programme unique" 81 | }, 82 | "restart": { 83 | "description": "Redémarre l'appareil", 84 | "name": "Redémarrer l'appareil" 85 | }, 86 | "schedule": { 87 | "description": "Définir ou modifier le programme de la tondeuse", 88 | "fields": { 89 | "friday_boundary": { 90 | "description": "Faut-il commencer le programme par couper la bordure (routine de bord) ?", 91 | "name": "Vendredi, Bord" 92 | }, 93 | "friday_end": { 94 | "description": "Quand le programme doit-il s'arrêter le vendredi ?", 95 | "name": "Vendredi, Fin" 96 | }, 97 | "friday_start": { 98 | "description": "Heure de départ le vendredi", 99 | "name": "Vendredi, Début" 100 | }, 101 | "monday_boundary": { 102 | "description": "Faut-il commencer le programme par couper la bordure (routine de bord) ?", 103 | "name": "Lundi, Bord" 104 | }, 105 | "monday_end": { 106 | "description": "Quand le programme doit-il s'arrêter le lundi ?", 107 | "name": "Lundi, Fin" 108 | }, 109 | "monday_start": { 110 | "description": "Heure de départ le lundi", 111 | "name": "Lundi, Début" 112 | }, 113 | "saturday_boundary": { 114 | "description": "Faut-il commencer le programme par couper la bordure (routine de bord) ?", 115 | "name": "Samedi, Bord" 116 | }, 117 | "saturday_end": { 118 | "description": "Quand le programme doit-il s'arrêter le samedi ?", 119 | "name": "Samedi, Fin" 120 | }, 121 | "saturday_start": { 122 | "description": "Heure de départ le samedi", 123 | "name": "Samedi, Début" 124 | }, 125 | "sunday_boundary": { 126 | "description": "Faut-il commencer le programme par couper la bordure (routine de bord) ?", 127 | "name": "Dimanche, Bord" 128 | }, 129 | "sunday_end": { 130 | "description": "Quand le programme doit-il s'arrêter le dimanche ?", 131 | "name": "Dimanche, Fin" 132 | }, 133 | "sunday_start": { 134 | "description": "Heure de départ le dimanche", 135 | "name": "Dimanche, Début" 136 | }, 137 | "thursday_boundary": { 138 | "description": "Faut-il commencer le programme par couper la bordure (routine de bord) ?", 139 | "name": "Jeudi, Bord" 140 | }, 141 | "thursday_end": { 142 | "description": "Quand le programme doit-il s'arrêter le jeudi ?", 143 | "name": "Jeudi, Fin" 144 | }, 145 | "thursday_start": { 146 | "description": "Heure de départ le jeudi", 147 | "name": "Mercredi, Début" 148 | }, 149 | "tuesday_boundary": { 150 | "description": "Faut-il commencer le programme par couper la bordure (routine de bord) ?", 151 | "name": "Mardi, Bord" 152 | }, 153 | "tuesday_end": { 154 | "description": "Quand le programme doit-il s'arrêter le mardi ?", 155 | "name": "Mardi, Fin" 156 | }, 157 | "tuesday_start": { 158 | "description": "Heure de départ le mardi", 159 | "name": "Mardi, Début" 160 | }, 161 | "type": { 162 | "description": "Changer le programme principal ou secondaire ?", 163 | "name": "Type de programme" 164 | }, 165 | "wednesday_boundary": { 166 | "description": "Faut-il commencer le programme par couper la bordure (routine de bord) ?", 167 | "name": "Mercredi, Bord" 168 | }, 169 | "wednesday_end": { 170 | "description": "Quand le programme doit-il s'arrêter le mercredi ?", 171 | "name": "Mercredi, Fin" 172 | }, 173 | "wednesday_start": { 174 | "description": "Heure de départ le mercredi", 175 | "name": "Mercredi, Début" 176 | } 177 | }, 178 | "name": "Définir ou modifier le programme" 179 | }, 180 | "send_raw": { 181 | "description": "Envoyer une commande JSON brute à l'appareil", 182 | "fields": { 183 | "json": { 184 | "description": "Données à envoyer, formatées en JSON valide", 185 | "name": "Données JSON" 186 | } 187 | }, 188 | "name": "Envoyer la commande RAW" 189 | }, 190 | "setzone": { 191 | "description": "Définir la zone à tondre ensuite", 192 | "fields": { 193 | "zone": { 194 | "description": "Définit le numéro de zone, allant de 0 à 3, à tondre ensuite", 195 | "name": "Zone" 196 | } 197 | }, 198 | "name": "Définir la zone" 199 | }, 200 | "torque": { 201 | "name": "Couple" 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/ro_RO.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Acest cont a fost deja configurat" 5 | }, 6 | "error": { 7 | "cannot_connect": "A eșuat conectarea", 8 | "invalid_auth": "Eroare la autentificare", 9 | "unknown": "Eroare neașteptată" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "email": "E-mail", 15 | "password": "Parolă", 16 | "type": "Marcă" 17 | }, 18 | "title": "Conectează-te la contul tău Landroid Cloud" 19 | } 20 | } 21 | }, 22 | "entity": { 23 | "lawn_mower": { 24 | "landroid_cloud": { 25 | "state": { 26 | "edgecut": "Tunde perimetrul", 27 | "escaped_digital_fence": "A trecut peste gardul digital", 28 | "initializing": "Se inițializează", 29 | "mowing": "Cosire", 30 | "offline": "Deconectat", 31 | "rain_delay": "Întârziere din cauza ploii", 32 | "returning": "Întoarcere la încărcător", 33 | "searching_zone": "În căutarea zonei", 34 | "starting": "Pornește", 35 | "zoning": "Antrenament zone" 36 | } 37 | } 38 | } 39 | }, 40 | "services": { 41 | "config": { 42 | "description": "Setează parametrii de configurare a dispozitivului", 43 | "fields": { 44 | "multizone_distances": { 45 | "description": "Setează matricea de distanțe multi-zonă, în metri. 0 = Dezactivat. Format: 15, 80, 120, 155", 46 | "name": "Distanțe multi-zonă" 47 | }, 48 | "multizone_probabilities": { 49 | "description": "Setează matricea de probabilități multi-zonă. Format: 50, 10, 20, 20", 50 | "name": "Probabilități multi-zonă" 51 | }, 52 | "raindelay": { 53 | "description": "Setează întârzierea cauzată de ploaie. Interval între 0 și 300 de minute. 0 = Dezactivat", 54 | "name": "Întârziere din cauza ploii" 55 | }, 56 | "timeextension": { 57 | "description": "Setează extensia de timp. Extensia este un procent între -100 și 100", 58 | "name": "Extensie de timp" 59 | } 60 | }, 61 | "name": "Setează zona" 62 | }, 63 | "edgecut": { 64 | "description": "Pornește tunderea perimetrului (dacă dispozivul permite)", 65 | "name": "Tunderea perimetrului" 66 | }, 67 | "ots": { 68 | "description": "Pornește programarea unică (dacă dispozitivul permite)", 69 | "fields": { 70 | "boundary": { 71 | "description": "Efectuează cosirea perimetrului", 72 | "name": "Perimetru" 73 | }, 74 | "runtime": { 75 | "description": "Timpul de funcționare până la întoarcerea la stația de încărcare", 76 | "name": "Timp de rulare" 77 | } 78 | }, 79 | "name": "Programare unică" 80 | }, 81 | "restart": { 82 | "description": "Repornește dispozitivul", 83 | "name": "Repornește dispozitivul" 84 | }, 85 | "schedule": { 86 | "description": "Setează sau schimbă programul robotului de tuns iarba", 87 | "fields": { 88 | "friday_boundary": { 89 | "description": "Ar trebui început programul cu tunderea perimetrului (edge/border cut)?", 90 | "name": "Vineri, Perimetru" 91 | }, 92 | "friday_end": { 93 | "description": "Când ar trebui să se oprească programul vineri?", 94 | "name": "Vineri, Sfârșit" 95 | }, 96 | "friday_start": { 97 | "description": "Ora de începere pentru vineri", 98 | "name": "Vineri, Start" 99 | }, 100 | "monday_boundary": { 101 | "description": "Ar trebui început programul cu tunderea perimetrului (edge/border cut)?", 102 | "name": "Luni, Perimetru" 103 | }, 104 | "monday_end": { 105 | "description": "Când ar trebui să se oprească programul luni?", 106 | "name": "Luni, Sfârșit" 107 | }, 108 | "monday_start": { 109 | "description": "Ora de începere pentru luni", 110 | "name": "Luni, Start" 111 | }, 112 | "saturday_boundary": { 113 | "description": "Ar trebui început programul cu tunderea perimetrului (edge/border cut)?", 114 | "name": "Sâmbătă, Perimetru" 115 | }, 116 | "saturday_end": { 117 | "description": "Când ar trebui să se oprească programul sâmbăta?", 118 | "name": "Sâmbătă, Sfârșit" 119 | }, 120 | "saturday_start": { 121 | "description": "Ora de începere pentru sâmbătă", 122 | "name": "Sâmbătă, Start" 123 | }, 124 | "sunday_boundary": { 125 | "description": "Ar trebui început programul cu tunderea perimetrului (edge/border cut)?", 126 | "name": "Duminică, Perimetru" 127 | }, 128 | "sunday_end": { 129 | "description": "Când ar trebui să se oprească programul duminica?", 130 | "name": "Duminică, Sfârșit" 131 | }, 132 | "sunday_start": { 133 | "description": "Ora de începere pentru duminică", 134 | "name": "Duminică, Start" 135 | }, 136 | "thursday_boundary": { 137 | "description": "Ar trebui început programul cu tunderea perimetrului (edge/border cut)?", 138 | "name": "Joi, Perimetru" 139 | }, 140 | "thursday_end": { 141 | "description": "Când ar trebui să se oprească programul joi?", 142 | "name": "Joi, Sfârșit" 143 | }, 144 | "thursday_start": { 145 | "description": "Ora de începere pentru joi", 146 | "name": "Joi, Start" 147 | }, 148 | "tuesday_boundary": { 149 | "description": "Ar trebui început programul cu tunderea perimetrului (edge/border cut)?", 150 | "name": "Marți, Perimetru" 151 | }, 152 | "tuesday_end": { 153 | "description": "Când ar trebui să se oprească programul marți?", 154 | "name": "Marți, Sfârșit" 155 | }, 156 | "tuesday_start": { 157 | "description": "Ora de începere pentru marți", 158 | "name": "Marți, Start" 159 | }, 160 | "type": { 161 | "description": "Schimb programul primar sau secundar?", 162 | "name": "Tip de program" 163 | }, 164 | "wednesday_boundary": { 165 | "description": "Ar trebui început programul cu tunderea perimetrului (edge/border cut)?", 166 | "name": "Miercuri, Perimetru" 167 | }, 168 | "wednesday_end": { 169 | "description": "Când ar trebui să se oprească programul miercuri?", 170 | "name": "Miercuri, Sfârșit" 171 | }, 172 | "wednesday_start": { 173 | "description": "Ora de începere pentru miercuri", 174 | "name": "Miercuri, Start" 175 | } 176 | }, 177 | "name": "Setează sau actualizează programul" 178 | }, 179 | "send_raw": { 180 | "description": "Trimite o comandă JSON brută către dispozitiv", 181 | "fields": { 182 | "json": { 183 | "description": "Date de trimis, formatate ca JSON valid", 184 | "name": "Date JSON" 185 | } 186 | }, 187 | "name": "Trimite comandă brută" 188 | }, 189 | "setzone": { 190 | "description": "Selectează zona ce urmează a fi cosită", 191 | "fields": { 192 | "zone": { 193 | "description": "Setează numărul zonei, de la 1 la 3, ce urmează a fi cosită", 194 | "name": "Zonă" 195 | } 196 | }, 197 | "name": "Setează zona" 198 | }, 199 | "torque": { 200 | "description": "Setează cuplul roților (dacă dispozitivul permite)", 201 | "fields": { 202 | "torque": { 203 | "description": "Setează cuplul roților. De la -50% la 50%", 204 | "name": "Cuplul la roată" 205 | } 206 | }, 207 | "name": "Cuplu" 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/sensor.py: -------------------------------------------------------------------------------- 1 | """Sensors for landroid_cloud.""" 2 | 3 | from __future__ import annotations 4 | 5 | import pytz 6 | from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.const import EntityCategory 9 | from homeassistant.core import HomeAssistant 10 | from pyworxcloud import WorxCloud 11 | 12 | from .api import LandroidAPI 13 | from .const import ATTR_CLOUD, ATTR_DEVICES, DOMAIN, ERROR_MAP 14 | from .device_base import LandroidSensor, LandroidSensorEntityDescription 15 | 16 | SENSORS = [ 17 | LandroidSensorEntityDescription( 18 | key="battery_state", 19 | name="Battery", 20 | entity_category=None, 21 | state_class=SensorStateClass.MEASUREMENT, 22 | device_class=SensorDeviceClass.BATTERY, 23 | entity_registry_enabled_default=True, 24 | native_unit_of_measurement="%", 25 | value_fn=lambda landroid: landroid.battery.get("percent", None), 26 | attributes=["charging"], 27 | ), 28 | LandroidSensorEntityDescription( 29 | key="battery_temperature", 30 | name="Battery Temperature", 31 | entity_category=EntityCategory.DIAGNOSTIC, 32 | state_class=SensorStateClass.MEASUREMENT, 33 | device_class=SensorDeviceClass.TEMPERATURE, 34 | entity_registry_enabled_default=True, 35 | native_unit_of_measurement="°C", 36 | value_fn=lambda landroid: landroid.battery.get("temperature", None), 37 | ), 38 | LandroidSensorEntityDescription( 39 | key="battery_cycles_total", 40 | name="Battery Total Charge Cycles", 41 | entity_category=EntityCategory.DIAGNOSTIC, 42 | state_class=SensorStateClass.MEASUREMENT, 43 | device_class=None, 44 | entity_registry_enabled_default=True, 45 | native_unit_of_measurement=" ", 46 | value_fn=lambda landroid: ( 47 | landroid.battery["cycles"]["total"] 48 | if "cycles" in landroid.battery 49 | else None 50 | ), 51 | icon="mdi:battery-sync", 52 | ), 53 | LandroidSensorEntityDescription( 54 | key="battery_voltage", 55 | name="Battery Voltage", 56 | entity_category=EntityCategory.DIAGNOSTIC, 57 | state_class=SensorStateClass.MEASUREMENT, 58 | device_class=SensorDeviceClass.VOLTAGE, 59 | entity_registry_enabled_default=False, 60 | native_unit_of_measurement="V", 61 | value_fn=lambda landroid: landroid.battery.get("voltage", None), 62 | ), 63 | LandroidSensorEntityDescription( 64 | key="blades_total_on", 65 | name="Blades Total On Time", 66 | entity_category=EntityCategory.DIAGNOSTIC, 67 | state_class=SensorStateClass.MEASUREMENT, 68 | device_class=SensorDeviceClass.DURATION, 69 | entity_registry_enabled_default=True, 70 | native_unit_of_measurement="h", 71 | suggested_display_precision=0, 72 | value_fn=lambda landroid: ( 73 | round(landroid.blades["total_on"] / 60, 2) 74 | if "total_on" in landroid.blades 75 | else None 76 | ), 77 | icon="mdi:saw-blade", 78 | attributes=["total_on"], 79 | ), 80 | LandroidSensorEntityDescription( 81 | key="blades_current_on", 82 | name="Blades Current On Time", 83 | entity_category=EntityCategory.DIAGNOSTIC, 84 | state_class=SensorStateClass.MEASUREMENT, 85 | device_class=SensorDeviceClass.DURATION, 86 | entity_registry_enabled_default=True, 87 | native_unit_of_measurement="h", 88 | suggested_display_precision=0, 89 | value_fn=lambda landroid: ( 90 | round(landroid.blades["current_on"] / 60, 2) 91 | if "current_on" in landroid.blades 92 | else None 93 | ), 94 | icon="mdi:saw-blade", 95 | attributes=["current_on"], 96 | ), 97 | LandroidSensorEntityDescription( 98 | key="blades_reset_at", 99 | name="Blades Reset At Hours", 100 | entity_category=EntityCategory.DIAGNOSTIC, 101 | state_class=None, 102 | device_class=SensorDeviceClass.DURATION, 103 | entity_registry_enabled_default=False, 104 | native_unit_of_measurement="h", 105 | suggested_display_precision=0, 106 | value_fn=lambda landroid: ( 107 | round(landroid.blades["reset_at"] / 60, 2) 108 | if "reset_at" in landroid.blades and landroid.blades["reset_at"] is not None 109 | else None 110 | ), 111 | icon="mdi:history", 112 | attributes=["reset_at"], 113 | ), 114 | LandroidSensorEntityDescription( 115 | key="blades_reset_time", 116 | name="Blades Reset At", 117 | entity_category=EntityCategory.DIAGNOSTIC, 118 | state_class=None, 119 | device_class=SensorDeviceClass.TIMESTAMP, 120 | entity_registry_enabled_default=False, 121 | native_unit_of_measurement=None, 122 | value_fn=lambda landroid: landroid.blades.get("reset_time", None), 123 | ), 124 | LandroidSensorEntityDescription( 125 | key="error", 126 | name="Error", 127 | translation_key="landroid_cloud_error", 128 | entity_category=EntityCategory.DIAGNOSTIC, 129 | state_class=None, 130 | device_class=SensorDeviceClass.ENUM, 131 | entity_registry_enabled_default=True, 132 | native_unit_of_measurement=None, 133 | value_fn=lambda landroid: ERROR_MAP[landroid.error["id"]], 134 | attributes=["id"], 135 | icon="mdi:alert-circle", 136 | ), 137 | LandroidSensorEntityDescription( 138 | key="pitch", 139 | name="Pitch", 140 | entity_category=None, 141 | state_class=SensorStateClass.MEASUREMENT, 142 | device_class=None, 143 | entity_registry_enabled_default=True, 144 | native_unit_of_measurement="°", 145 | value_fn=lambda landroid: landroid.orientation["pitch"], 146 | suggested_display_precision=1, 147 | icon="mdi:axis-x-rotate-clockwise", 148 | ), 149 | LandroidSensorEntityDescription( 150 | key="roll", 151 | name="Roll", 152 | entity_category=None, 153 | state_class=SensorStateClass.MEASUREMENT, 154 | device_class=None, 155 | entity_registry_enabled_default=True, 156 | native_unit_of_measurement="°", 157 | value_fn=lambda landroid: landroid.orientation["roll"], 158 | suggested_display_precision=1, 159 | icon="mdi:axis-y-rotate-clockwise", 160 | ), 161 | LandroidSensorEntityDescription( 162 | key="yaw", 163 | name="Yaw", 164 | entity_category=None, 165 | state_class=SensorStateClass.MEASUREMENT, 166 | device_class=None, 167 | entity_registry_enabled_default=True, 168 | native_unit_of_measurement="°", 169 | value_fn=lambda landroid: landroid.orientation["yaw"], 170 | suggested_display_precision=1, 171 | icon="mdi:axis-z-rotate-clockwise", 172 | ), 173 | LandroidSensorEntityDescription( 174 | key="rainsensor_remaining", 175 | name="Rainsensor Remaining", 176 | entity_category=EntityCategory.DIAGNOSTIC, 177 | state_class=None, 178 | device_class=SensorDeviceClass.DURATION, 179 | entity_registry_enabled_default=False, 180 | native_unit_of_measurement="min", 181 | value_fn=lambda landroid: landroid.rainsensor.get("remaining", None), 182 | icon="mdi:weather-rainy", 183 | ), 184 | LandroidSensorEntityDescription( 185 | key="distance", 186 | name="Distance Driven", 187 | entity_category=EntityCategory.DIAGNOSTIC, 188 | state_class=SensorStateClass.MEASUREMENT, 189 | device_class=SensorDeviceClass.DISTANCE, 190 | entity_registry_enabled_default=True, 191 | native_unit_of_measurement="km", 192 | suggested_display_precision=2, 193 | value_fn=lambda landroid: ( 194 | round(landroid.statistics["distance"] / 1000, 3) 195 | if "distance" in landroid.statistics 196 | else None 197 | ), 198 | attributes=["distance"], 199 | ), 200 | LandroidSensorEntityDescription( 201 | key="worktime_total", 202 | name="Total Worktime", 203 | entity_category=EntityCategory.DIAGNOSTIC, 204 | state_class=SensorStateClass.MEASUREMENT, 205 | device_class=SensorDeviceClass.DURATION, 206 | entity_registry_enabled_default=True, 207 | native_unit_of_measurement="h", 208 | suggested_display_precision=0, 209 | value_fn=lambda landroid: ( 210 | round(landroid.statistics["worktime_total"] / 60, 2) 211 | if "worktime_total" in landroid.statistics 212 | else None 213 | ), 214 | icon="mdi:update", 215 | attributes=["worktime_total"], 216 | ), 217 | LandroidSensorEntityDescription( 218 | key="rssi", 219 | name="Rssi", 220 | entity_category=EntityCategory.DIAGNOSTIC, 221 | state_class=SensorStateClass.MEASUREMENT, 222 | device_class=SensorDeviceClass.SIGNAL_STRENGTH, 223 | entity_registry_enabled_default=True, 224 | native_unit_of_measurement="dBm", 225 | value_fn=lambda landroid: landroid.rssi, 226 | ), 227 | LandroidSensorEntityDescription( 228 | key="last_update", 229 | name="Last Update", 230 | entity_category=None, 231 | state_class=None, 232 | device_class=SensorDeviceClass.TIMESTAMP, 233 | entity_registry_enabled_default=True, 234 | native_unit_of_measurement=None, 235 | value_fn=lambda landroid: landroid.updated.astimezone(pytz.utc), 236 | icon="mdi:clock-check", 237 | ), 238 | LandroidSensorEntityDescription( 239 | key="next_start", 240 | name="Next Scheduled Start", 241 | entity_category=None, 242 | state_class=None, 243 | device_class=SensorDeviceClass.TIMESTAMP, 244 | entity_registry_enabled_default=True, 245 | native_unit_of_measurement=None, 246 | value_fn=lambda landroid: landroid.schedules["next_schedule_start"], 247 | icon="mdi:clock-start", 248 | attributes=["schedule"], 249 | ), 250 | LandroidSensorEntityDescription( 251 | key="daily_progress", 252 | name="Daily Progress", 253 | entity_category=None, 254 | state_class=None, 255 | device_class=None, 256 | entity_registry_enabled_default=False, 257 | native_unit_of_measurement="%", 258 | value_fn=lambda landroid: landroid.schedules["daily_progress"], 259 | icon="mdi:progress-clock", 260 | ), 261 | ] 262 | 263 | 264 | async def async_setup_entry( 265 | hass: HomeAssistant, 266 | config: ConfigEntry, 267 | async_add_devices, 268 | ) -> None: 269 | """Set up the sensor platform.""" 270 | sensors = [] 271 | cloud: WorxCloud = hass.data[DOMAIN][config.entry_id][ATTR_CLOUD] 272 | for _, mower in cloud.devices.items(): 273 | api = LandroidAPI(hass, mower.name, config) 274 | # for _, info in hass.data[DOMAIN][config.entry_id][ATTR_DEVICES].items(): 275 | # api: LandroidAPI = info["api"] 276 | for sens in SENSORS: 277 | entity = LandroidSensor(hass, sens, api, config) 278 | 279 | sensors.append(entity) 280 | 281 | async_add_devices(sensors) 282 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/const.py: -------------------------------------------------------------------------------- 1 | """Constants used by Landroid Cloud integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import inspect 6 | from dataclasses import dataclass 7 | from enum import IntEnum 8 | 9 | from homeassistant.components.lawn_mower import LawnMowerActivity 10 | from homeassistant.components.lock import LockState 11 | from homeassistant.const import STATE_IDLE, STATE_UNKNOWN 12 | from pyworxcloud.clouds import CloudType 13 | from pyworxcloud.utils import DeviceCapability 14 | 15 | from .utils.logger import LogLevel 16 | 17 | # Startup banner 18 | STARTUP = """ 19 | ------------------------------------------------------------------- 20 | Landroid Cloud integration 21 | 22 | Version: %s 23 | This is a custom integration 24 | If you have any issues with this you need to open an issue here: 25 | https://github.com/mtrab/landroid_cloud/issues 26 | ------------------------------------------------------------------- 27 | """ 28 | 29 | # Some defaults 30 | DEFAULT_NAME = "landroid" 31 | DOMAIN = "landroid_cloud" 32 | PLATFORMS_SECONDARY = [] 33 | PLATFORMS_PRIMARY = [ 34 | "lawn_mower", 35 | "sensor", 36 | "switch", 37 | "binary_sensor", 38 | "number", 39 | "button", 40 | "select", 41 | ] 42 | UPDATE_SIGNAL = "landroid_cloud_update" 43 | LOGLEVEL = LogLevel.DEBUG 44 | ENTITY_ID_FORMAT = DOMAIN + ".{}" 45 | 46 | # Redact consts 47 | REDACT_TITLE = "title" 48 | 49 | # Service consts 50 | SERVICE_CONFIG = "config" 51 | SERVICE_SETZONE = "setzone" 52 | SERVICE_OTS = "ots" 53 | SERVICE_SCHEDULE = "schedule" 54 | SERVICE_SEND_RAW = "send_raw" 55 | 56 | # Extra states 57 | STATE_BATTERY_LOW = "battery_low" 58 | STATE_BATTERY_OPEN = "battery_trunk_open_timeout" 59 | STATE_BATTERY_TEMPERATURE_ERROR = "battery_temperature_error" 60 | STATE_BLADE_BLOCKED = "blade_motor_blocked" 61 | STATE_CAMERA_ERROR = "camera_error" 62 | STATE_CHARGING_ERROR = "charge_error" 63 | STATE_CLOSE_DOOR_HOME = "close_door_to_go_home" 64 | STATE_CLOSE_DOOR_MOW = "close_door_to_mow" 65 | STATE_DOCKING_ERROR = "charging_station_docking_error" 66 | STATE_EDGECUT = "edgecut" 67 | STATE_ESCAPED_DIGITAL_FENCE = "escaped_digital_fence" 68 | STATE_EXESSIVE_SLOPE = "excessive_slope" 69 | STATE_HEADLIGHT_ERROR = "headlight_error" 70 | STATE_HEIGHT_BLOCKED = "blade_height_adjustment_blocked" 71 | STATE_HBI_ERROR = "hbi_error" 72 | STATE_INITIALIZING = "initializing" 73 | STATE_INSUFFICIENT_SENSOR_DATA = "insufficient_sensor_data" 74 | STATE_LIFTED = "lifted" 75 | STATE_MAP_ERROR = "map_error" 76 | STATE_MAPPING_FAILED = "mapping_exploration_failed" 77 | STATE_MAPPING_REQUIRED = "mapping_exploration_required" 78 | STATE_MISSING_DOCK = "missing_charging_station" 79 | STATE_NO_ERROR = "no_error" 80 | STATE_OFFLINE = "offline" 81 | STATE_OTA_ERROR = "ota_error" 82 | STATE_OUTSIDE_WIRE = "outside_wire" 83 | STATE_RAINDELAY = "rain_delay" 84 | STATE_RETURNING = "returning" 85 | STATE_REVERSE_WIRE = "reverse_wire" 86 | STATE_RFID_ERROR = "rfid_reader_error" 87 | STATE_SEARCHING_ZONE = "searching_zone" 88 | STATE_STARTING = "starting" 89 | STATE_TIMEOUT_HOME = "timeout_finding_home" 90 | STATE_TRAINING_START_DISALLOWED = "training_start_disallowed" 91 | STATE_TRAPPED = "trapped" 92 | STATE_TRAPPED_TIMEOUT = "trapped_timeout" 93 | STATE_UNREACHABLE_DOCK = "unreachable_charging_station" 94 | STATE_UNREACHABLE_ZONE = "unreachable_zone" 95 | STATE_UPSIDE_DOWN = "upside_down" 96 | STATE_ZONING = "zoning" 97 | STATE_WHEEL_BLOCKED = "wheel_motor_blocked" 98 | STATE_WIRE_MISSING = "wire_missing" 99 | STATE_WIRE_SYNC = "wire_sync" 100 | 101 | 102 | # Service attributes 103 | ATTR_MULTIZONE_DISTANCES = "multizone_distances" 104 | ATTR_MULTIZONE_PROBABILITIES = "multizone_probabilities" 105 | ATTR_RAINDELAY = "raindelay" 106 | ATTR_TIMEEXTENSION = "timeextension" 107 | ATTR_ZONE = "zone" 108 | ATTR_BOUNDARY = "boundary" 109 | ATTR_JSON = "json" 110 | ATTR_RUNTIME = "runtime" 111 | ATTR_TORQUE = "torque" 112 | ATTR_SERVICES = "services" 113 | ATTR_SERVICE = "service" 114 | 115 | # Attributes used for managing schedules 116 | ATTR_TYPE = "type" 117 | ATTR_MONDAY_START = "monday_start" 118 | ATTR_MONDAY_END = "monday_end" 119 | ATTR_MONDAY_BOUNDARY = "monday_boundary" 120 | ATTR_TUESDAY_START = "tuesday_start" 121 | ATTR_TUESDAY_END = "tuesday_end" 122 | ATTR_TUESDAY_BOUNDARY = "tuesday_boundary" 123 | ATTR_WEDNESDAY_START = "wednesday_start" 124 | ATTR_WEDNESDAY_END = "wednesday_end" 125 | ATTR_WEDNESDAY_BOUNDARY = "wednesday_boundary" 126 | ATTR_THURSDAY_START = "thursday_start" 127 | ATTR_THURSDAY_END = "thursday_end" 128 | ATTR_THURSDAY_BOUNDARY = "thursday_boundary" 129 | ATTR_FRIDAY_START = "friday_start" 130 | ATTR_FRIDAY_END = "friday_end" 131 | ATTR_FRIDAY_BOUNDARY = "friday_boundary" 132 | ATTR_SATURDAY_START = "saturday_start" 133 | ATTR_SATURDAY_END = "saturday_end" 134 | ATTR_SATURDAY_BOUNDARY = "saturday_boundary" 135 | ATTR_SUNDAY_START = "sunday_start" 136 | ATTR_SUNDAY_END = "sunday_end" 137 | ATTR_SUNDAY_BOUNDARY = "sunday_boundary" 138 | 139 | # Entity extra attributes 140 | ATTR_ACCESSORIES = "accessories" 141 | ATTR_BATTERY = "battery" 142 | ATTR_BLADES = "blades" 143 | ATTR_CAPABILITIES = "capabilities" 144 | ATTR_ERROR = "error" 145 | ATTR_FIRMWARE = "firmware" 146 | ATTR_LANDROIDFEATURES = "supported_landroid_features" 147 | ATTR_LATITUDE = "latitude" 148 | ATTR_LONGITUDE = "longitude" 149 | ATTR_LAWN = "lawn" 150 | ATTR_MACADDRESS = "mac_address" 151 | ATTR_MQTTCONNECTED = "mqtt_connected" 152 | ATTR_ONLINE = "online" 153 | ATTR_ORIENTATION = "orientation" 154 | ATTR_RAINSENSOR = "rain_sensor" 155 | ATTR_REGISTERED = "registered_at" 156 | ATTR_SCHEDULE = "schedule" 157 | ATTR_SERIAL = "serial_number" 158 | ATTR_STATISTICS = "statistics" 159 | ATTR_TIMEZONE = "time_zone" 160 | ATTR_UPDATED = "state_updated_at" 161 | ATTR_WARRANTY = "warranty" 162 | ATTR_PARTYMODE = "party_mode_enabled" 163 | ATTR_RSSI = "rssi" 164 | ATTR_STATUS = "status_info" 165 | ATTR_PROGRESS = "daily_progress" 166 | ATTR_NEXT_SCHEDULE = "next_scheduled_start" 167 | 168 | # Misc. attributes 169 | ATTR_NEXT_ZONE = "next_zone" 170 | ATTR_CLOUD = "cloud" 171 | ATTR_DEVICES = "devices" 172 | ATTR_DEVICEIDS = "device_ids" 173 | ATTR_DEVICE = "device" 174 | ATTR_API = "api" 175 | ATTR_FEATUREBITS = "feature_bits" 176 | 177 | # Available cloud vendors 178 | CLOUDS = [] 179 | for name, cloud in inspect.getmembers(CloudType): 180 | if inspect.isclass(cloud) and "__" not in name: 181 | CLOUDS.append(name.capitalize()) 182 | 183 | # State mapping 184 | STATE_MAP = { 185 | 0: STATE_IDLE, 186 | 1: LawnMowerActivity.DOCKED, 187 | 2: STATE_STARTING, 188 | 3: STATE_STARTING, 189 | 4: STATE_RETURNING, 190 | 5: STATE_RETURNING, 191 | 6: STATE_RETURNING, 192 | 7: LawnMowerActivity.MOWING, 193 | 8: LawnMowerActivity.ERROR, 194 | 9: LawnMowerActivity.ERROR, 195 | 10: LawnMowerActivity.ERROR, 196 | 11: LawnMowerActivity.ERROR, 197 | 12: LawnMowerActivity.MOWING, 198 | 13: STATE_ESCAPED_DIGITAL_FENCE, 199 | 30: STATE_RETURNING, 200 | 31: STATE_ZONING, 201 | 32: STATE_EDGECUT, 202 | 33: STATE_STARTING, 203 | 34: LawnMowerActivity.PAUSED, 204 | 103: STATE_SEARCHING_ZONE, 205 | 104: STATE_RETURNING, 206 | } 207 | 208 | # Error mapping 209 | ERROR_MAP = { 210 | -1: STATE_UNKNOWN, 211 | 0: STATE_NO_ERROR, 212 | 1: STATE_TRAPPED, 213 | 2: STATE_LIFTED, 214 | 3: STATE_WIRE_MISSING, 215 | 4: STATE_OUTSIDE_WIRE, 216 | 5: STATE_RAINDELAY, 217 | 6: STATE_CLOSE_DOOR_MOW, 218 | 7: STATE_CLOSE_DOOR_HOME, 219 | 8: STATE_BLADE_BLOCKED, 220 | 9: STATE_WHEEL_BLOCKED, 221 | 10: STATE_TRAPPED_TIMEOUT, 222 | 11: STATE_UPSIDE_DOWN, 223 | 12: STATE_BATTERY_LOW, 224 | 13: STATE_REVERSE_WIRE, 225 | 14: STATE_CHARGING_ERROR, 226 | 15: STATE_TIMEOUT_HOME, 227 | 16: LockState.LOCKED, 228 | 17: STATE_BATTERY_TEMPERATURE_ERROR, 229 | 19: STATE_BATTERY_OPEN, 230 | 20: STATE_WIRE_SYNC, 231 | 100: STATE_DOCKING_ERROR, 232 | 101: STATE_HBI_ERROR, 233 | 102: STATE_OTA_ERROR, 234 | 103: STATE_MAP_ERROR, 235 | 104: STATE_EXESSIVE_SLOPE, 236 | 105: STATE_UNREACHABLE_ZONE, 237 | 106: STATE_UNREACHABLE_DOCK, 238 | 108: STATE_INSUFFICIENT_SENSOR_DATA, 239 | 109: STATE_TRAINING_START_DISALLOWED, 240 | 110: STATE_CAMERA_ERROR, 241 | 111: STATE_MAPPING_REQUIRED, 242 | 112: STATE_MAPPING_FAILED, 243 | 113: STATE_RFID_ERROR, 244 | 114: STATE_HEADLIGHT_ERROR, 245 | 115: STATE_MISSING_DOCK, 246 | 116: STATE_HEIGHT_BLOCKED, 247 | } 248 | 249 | 250 | @dataclass 251 | class ScheduleDays(IntEnum): 252 | """Schedule types.""" 253 | 254 | SUNDAY = 0 255 | MONDAY = 1 256 | TUESDAY = 2 257 | WEDNESDAY = 3 258 | THURSDAY = 4 259 | FRIDAY = 5 260 | SATURDAY = 6 261 | 262 | 263 | # Map schedule to Landroid JSON property 264 | SCHEDULE_TYPE_MAP = { 265 | "primary": "d", 266 | "secondary": "dd", 267 | } 268 | 269 | 270 | # Map schedule days 271 | SCHEDULE_TO_DAY = { 272 | "sunday": { 273 | "day": ScheduleDays.SUNDAY, 274 | "start": ATTR_SUNDAY_START, 275 | "end": ATTR_SUNDAY_END, 276 | "boundary": ATTR_SUNDAY_BOUNDARY, 277 | "clear": "sunday", 278 | }, 279 | "monday": { 280 | "day": ScheduleDays.MONDAY, 281 | "start": ATTR_MONDAY_START, 282 | "end": ATTR_MONDAY_END, 283 | "boundary": ATTR_MONDAY_BOUNDARY, 284 | "clear": "monday", 285 | }, 286 | "tuesday": { 287 | "day": ScheduleDays.TUESDAY, 288 | "start": ATTR_TUESDAY_START, 289 | "end": ATTR_TUESDAY_END, 290 | "boundary": ATTR_TUESDAY_BOUNDARY, 291 | "clear": "tuesday", 292 | }, 293 | "wednesday": { 294 | "day": ScheduleDays.WEDNESDAY, 295 | "start": ATTR_WEDNESDAY_START, 296 | "end": ATTR_WEDNESDAY_END, 297 | "boundary": ATTR_WEDNESDAY_BOUNDARY, 298 | "clear": "wednesday", 299 | }, 300 | "thursday": { 301 | "day": ScheduleDays.THURSDAY, 302 | "start": ATTR_THURSDAY_START, 303 | "end": ATTR_THURSDAY_END, 304 | "boundary": ATTR_THURSDAY_BOUNDARY, 305 | "clear": "thursday", 306 | }, 307 | "friday": { 308 | "day": ScheduleDays.FRIDAY, 309 | "start": ATTR_FRIDAY_START, 310 | "end": ATTR_FRIDAY_END, 311 | "boundary": ATTR_FRIDAY_BOUNDARY, 312 | "clear": "friday", 313 | }, 314 | "saturday": { 315 | "day": ScheduleDays.SATURDAY, 316 | "start": ATTR_SATURDAY_START, 317 | "end": ATTR_SATURDAY_END, 318 | "boundary": ATTR_SATURDAY_BOUNDARY, 319 | "clear": "saturday", 320 | }, 321 | } 322 | 323 | 324 | class LandroidFeatureSupport(IntEnum): 325 | """Supported features of the Landroid integration.""" 326 | 327 | MOWER = 1 328 | BUTTON = 2 329 | SELECT = 4 330 | SETZONE = 8 331 | RESTART = 16 332 | LOCK = 32 333 | OTS = 64 334 | EDGECUT = 128 335 | PARTYMODE = 256 336 | CONFIG = 512 337 | SCHEDULES = 1024 338 | TORQUE = 2048 339 | RAW = 4096 340 | OFFLIMITS = 8192 341 | ACS = 16384 342 | 343 | 344 | API_TO_INTEGRATION_FEATURE_MAP = { 345 | DeviceCapability.EDGE_CUT: LandroidFeatureSupport.EDGECUT, 346 | DeviceCapability.ONE_TIME_SCHEDULE: LandroidFeatureSupport.OTS, 347 | DeviceCapability.PARTY_MODE: LandroidFeatureSupport.PARTYMODE, 348 | DeviceCapability.TORQUE: LandroidFeatureSupport.TORQUE, 349 | DeviceCapability.OFF_LIMITS: LandroidFeatureSupport.OFFLIMITS, 350 | DeviceCapability.ACS: LandroidFeatureSupport.ACS, 351 | } 352 | -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Dieses Benutzerkonto wurde bereits konfiguriert" 5 | }, 6 | "error": { 7 | "cannot_connect": "Verbindung ist nicht möglich", 8 | "invalid_auth": "Fehler bei der Authentifizierung", 9 | "too_many_requests": "Zu viele API-Anfragen - In 24 Stunden erneut versuchen", 10 | "unknown": "Unerwarteter Fehler" 11 | }, 12 | "step": { 13 | "user": { 14 | "data": { 15 | "email": "E-Mail", 16 | "password": "Passwort", 17 | "type": "Marke" 18 | }, 19 | "title": "Verbinden Sie sich mit Ihrem Landroid Cloud-Konto" 20 | } 21 | } 22 | }, 23 | "entity": { 24 | "lawn_mower": { 25 | "landroid_cloud": { 26 | "state": { 27 | "edgecut": "Kantenschnitt", 28 | "escaped_digital_fence": "Außerhalb der Begrenzung", 29 | "idle": "Untätig", 30 | "initializing": "Initialisierung", 31 | "locked": "Gesperrt", 32 | "mowing": "Mähen", 33 | "offline": "Offline", 34 | "rain_delay": "Regenverzögerung", 35 | "returning": "Rückkehr zur Ladestation", 36 | "searching_zone": "Zone suchen", 37 | "starting": "Startet", 38 | "unknown": "Unbekannt", 39 | "zoning": "Zonentraining" 40 | } 41 | } 42 | }, 43 | "sensor": { 44 | "landroid_cloud_error": { 45 | "state": { 46 | "battery_low": "Batterie schwach", 47 | "battery_temperature_error": "Batterietemperaturfehler", 48 | "blade_height_adjustment_blocked": "Klingenhöhenverstellung blockiert", 49 | "blade_motor_blocked": "Klingenmotor ist blockiert", 50 | "camera_error": "Kamerafehler", 51 | "charging_station_docking_error": "Fehler beim Andocken der Ladestation", 52 | "close_door_to_go_home": "Schließe die Klappe zum Fahren zur Ladestation", 53 | "close_door_to_mow": "Schließe die Klappe zum Mähen", 54 | "headlight_error": "Kein Fehler", 55 | "insufficient_sensor_data": "Unzureichende Sensordaten", 56 | "lifted": "Angehoben", 57 | "locked": "Mäher ist gesperrt", 58 | "map_error": "Kartenfehler", 59 | "mapping_exploration_failed": "Kartierung fehlgeschlagen", 60 | "mapping_exploration_required": "Kartierung erforderlich", 61 | "missing_charging_station": "Ladestation nicht erreichbar", 62 | "no_error": "Kein Fehler", 63 | "outside_wire": "Außerhalb des Begrenzungskabels", 64 | "rain_delay": "Regenverzögerung aktiv", 65 | "rfid_reader_error": "Fehler im RFID-Lesegerät", 66 | "trapped": "Gefangen", 67 | "unknown": "Unbekannt", 68 | "unreachable_charging_station": "Ladestation nicht erreichbar", 69 | "unreachable_zone": "Unerreichbare Zone", 70 | "upside_down": "Mäher liegt auf dem Kopf", 71 | "wheel_motor_blocked": "Radmotor ist blockiert", 72 | "wire_missing": "Kabel fehlt" 73 | } 74 | } 75 | } 76 | }, 77 | "services": { 78 | "config": { 79 | "description": "Gerätekonfigurationsparameter festlegen", 80 | "fields": { 81 | "multizone_distances": { 82 | "description": "Legen Sie das Multizonen-Entfernungsabstand in Metern fest. 0 = Deaktiviert. Format: 15, 80, 120, 155", 83 | "name": "Multizonen Abstände" 84 | }, 85 | "multizone_probabilities": { 86 | "description": "Legen Sie das Multizonen-Wahrscheinlichkeits-Array fest. Format: 50, 10, 20, 20", 87 | "name": "Multizonen Wahrscheinlichkeit" 88 | }, 89 | "raindelay": { 90 | "description": "Regenverzögerung einstellen. Zeit zwischen 0 und 300 Minuten. 0 = Deaktiviert", 91 | "name": "Regenverzögerung" 92 | }, 93 | "timeextension": { 94 | "description": "Zeitverlängerung einstellen in % im Bereich von -100 bis 100", 95 | "name": "Zeitverlängerung" 96 | } 97 | }, 98 | "name": "Zone setzen" 99 | }, 100 | "edgecut": { 101 | "description": "Kantenschnitt starten (falls unterstützt)", 102 | "name": "Rand-/Kantenschnitt" 103 | }, 104 | "ots": { 105 | "description": "Einmalplan starten (falls unterstützt)", 106 | "fields": { 107 | "boundary": { 108 | "description": "Rasenkannte schneiden", 109 | "name": "Kantenschnitt" 110 | }, 111 | "runtime": { 112 | "description": "Laufzeit in Minuten bis zur Rückkehr zur Ladestation", 113 | "name": "Laufzeit" 114 | } 115 | }, 116 | "name": "Einmaliger Zeitplan" 117 | }, 118 | "restart": { 119 | "description": "Startet das Gerät neu oder startet es neu", 120 | "name": "Gerät neustarten" 121 | }, 122 | "schedule": { 123 | "description": "Zeitplan des Mähers einstellen oder ändern.", 124 | "fields": { 125 | "friday_boundary": { 126 | "description": "Sollten wir diesen Zeitplan mit dem Schneiden der Begrenzung (Kanten-/Randschnitt) beginnen?", 127 | "name": "Freitag, Kantenschnitt" 128 | }, 129 | "friday_end": { 130 | "description": "Wann soll der Zeitplan freitags enden?", 131 | "name": "Freitag, Ende" 132 | }, 133 | "friday_start": { 134 | "description": "Startzeit für Freitage", 135 | "name": "Freitag, Beginn" 136 | }, 137 | "monday_boundary": { 138 | "description": "Sollten wir diesen Zeitplan mit dem Schneiden der Begrenzung (Kanten-/Randschnitt) beginnen?", 139 | "name": "Montag, Kantenschnitt" 140 | }, 141 | "monday_end": { 142 | "description": "Wann soll der Zeitplan montags enden?", 143 | "name": "Montag, Ende" 144 | }, 145 | "monday_start": { 146 | "description": "Startzeit für Montage", 147 | "name": "Montag, Beginn" 148 | }, 149 | "saturday_boundary": { 150 | "description": "Sollten wir diesen Zeitplan mit dem Schneiden der Begrenzung (Kanten-/Randschnitt) beginnen?", 151 | "name": "Samstag, Kantenschnitt" 152 | }, 153 | "saturday_end": { 154 | "description": "Wann soll der Zeitplan samstags enden?", 155 | "name": "Samstag, Ende" 156 | }, 157 | "saturday_start": { 158 | "description": "Startzeit für Samstage", 159 | "name": "Samstag, Beginn" 160 | }, 161 | "sunday_boundary": { 162 | "description": "Sollten wir diesen Zeitplan mit dem Schneiden der Begrenzung (Kanten-/Randschnitt) beginnen?", 163 | "name": "Sonntag, Kantenschnitt" 164 | }, 165 | "sunday_end": { 166 | "description": "Wann soll der Zeitplan sonntags enden?", 167 | "name": "Sonntag, Ende" 168 | }, 169 | "sunday_start": { 170 | "description": "Startzeit für Sonntage", 171 | "name": "Sonntag, Beginn" 172 | }, 173 | "thursday_boundary": { 174 | "description": "Sollten wir diesen Zeitplan mit dem Schneiden der Begrenzung (Kanten-/Randschnitt) beginnen?", 175 | "name": "Donnerstag, Kantenschnitt" 176 | }, 177 | "thursday_end": { 178 | "description": "Wann soll der Zeitplan donnerstags enden?", 179 | "name": "Donnerstag, Ende" 180 | }, 181 | "thursday_start": { 182 | "description": "Startzeit für Donnerstage", 183 | "name": "Mittwochs, Beginn" 184 | }, 185 | "tuesday_boundary": { 186 | "description": "Sollten wir diesen Zeitplan mit dem Schneiden der Begrenzung (Kanten-/Randschnitt) beginnen?", 187 | "name": "Dienstag, Kantenschnitt" 188 | }, 189 | "tuesday_end": { 190 | "description": "Wann soll der Zeitplan dienstags enden?", 191 | "name": "Dienstag, Ende" 192 | }, 193 | "tuesday_start": { 194 | "description": "Startzeit für Dienstage", 195 | "name": "Dienstag, Beginn" 196 | }, 197 | "type": { 198 | "description": "Primären oder sekundären Zeitplan ändern?", 199 | "name": "Zeitplantyp" 200 | }, 201 | "wednesday_boundary": { 202 | "description": "Sollten wir diesen Zeitplan mit dem Schneiden der Begrenzung (Kanten-/Randschnitt) beginnen?", 203 | "name": "Mittwoch, Kantenschnitt" 204 | }, 205 | "wednesday_end": { 206 | "description": "Wann soll der Zeitplan mittwochs enden?", 207 | "name": "Mittwoch, Ende" 208 | }, 209 | "wednesday_start": { 210 | "description": "Startzeit für Mittwoche", 211 | "name": "Mittwoch, Beginn" 212 | } 213 | }, 214 | "name": "Zeitplan festlegen oder aktualisieren" 215 | }, 216 | "send_raw": { 217 | "description": "Senden eines JSON-Befehls an das Gerät", 218 | "fields": { 219 | "json": { 220 | "description": "Zu sendende Daten, formatiert als gültiges JSON", 221 | "name": "JSON Daten" 222 | } 223 | }, 224 | "name": "RAW-Befehl senden" 225 | }, 226 | "setzone": { 227 | "description": "Zone setzen, die als nächstes gemäht werden soll", 228 | "fields": { 229 | "zone": { 230 | "description": "Legt die Zone im Bereich von 0 bis 3 fest, die als nächstes gemäht werden soll", 231 | "name": "Zone" 232 | } 233 | }, 234 | "name": "Zone setzen" 235 | }, 236 | "torque": { 237 | "description": "Raddrehmoment einstellen (falls unterstützt)", 238 | "fields": { 239 | "torque": { 240 | "description": "Raddrehmoment einstellen. Im Bereich von -50 % bis 50 %", 241 | "name": "Drehmoment" 242 | } 243 | }, 244 | "name": "Drehmoment" 245 | } 246 | } 247 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Det här användarkontot har redan konfigurerats" 5 | }, 6 | "error": { 7 | "cannot_connect": "Misslyckades med att ansluta", 8 | "invalid_auth": "Fel vid autentisering", 9 | "too_many_requests": "För många API-förfrågningar. Försök igen om 24 timmar", 10 | "unknown": "Oväntat fel" 11 | }, 12 | "step": { 13 | "user": { 14 | "data": { 15 | "email": "E-post", 16 | "password": "Lösenord", 17 | "type": "Modell" 18 | }, 19 | "title": "Anslut till ditt Landroid Cloud-konto" 20 | } 21 | } 22 | }, 23 | "entity": { 24 | "lawn_mower": { 25 | "landroid_cloud": { 26 | "state": { 27 | "edgecut": "Kantklippning", 28 | "escaped_digital_fence": "Utanför digital gräns", 29 | "idle": "Inaktiv", 30 | "initializing": "Initierar", 31 | "locked": "Låst", 32 | "mowing": "Klipper", 33 | "offline": "Offline", 34 | "rain_delay": "Regnfördröjning", 35 | "returning": "Återvänder hem", 36 | "searching_zone": "Söker efter zon", 37 | "starting": "Startar", 38 | "unknown": "Okänt", 39 | "zoning": "Zonträning" 40 | } 41 | } 42 | }, 43 | "sensor": { 44 | "landroid_cloud_error": { 45 | "state": { 46 | "battery_low": "Låg batterinivå", 47 | "battery_temperature_error": "Fel på batteriets temperatur", 48 | "battery_trunk_open_timeout": "Batterilucka öppen tidsgräns", 49 | "blade_height_adjustment_blocked": "Justering av bladhöjd blockerad", 50 | "blade_motor_blocked": "Bladmotorn är blockerad", 51 | "camera_error": "Fel på kameran", 52 | "charge_error": "Fel vid laddning", 53 | "charging_station_docking_error": "Fel vid dockning av laddstation", 54 | "close_door_to_go_home": "Stäng dörren för att köra hem", 55 | "close_door_to_mow": "Stäng dörren för att klippa", 56 | "excessive_slope": "För stor lutning upptäckt", 57 | "hbi_error": "HBI fel", 58 | "headlight_error": "Fel på strålkastare", 59 | "insufficient_sensor_data": "Otillräcklig sensordata", 60 | "lifted": "Gräsklipparen har lyfts", 61 | "locked": "Gräsklipparen är låst", 62 | "map_error": "Fel på kartan", 63 | "mapping_exploration_failed": "Kartläggning misslyckades", 64 | "mapping_exploration_required": "Kartläggning krävs", 65 | "missing_charging_station": "Saknar laddstation", 66 | "no_error": "Inget fel", 67 | "ota_error": "Fel på trådlös uppdatering", 68 | "outside_wire": "Utanför begränsningskabeln", 69 | "rain_delay": "Regnfördröjning aktiv", 70 | "reverse_wire": "Begränsningskabeln är felvänd", 71 | "rfid_reader_error": "RFID läsfel", 72 | "timeout_finding_home": "Kunde inte återvända hem inom tidsgränsen", 73 | "training_start_disallowed": "Ej möjligt att starta träning", 74 | "trapped": "Instängd", 75 | "trapped_timeout": "Instängd längre än tidsgränsen", 76 | "unknown": "Okänt", 77 | "unreachable_charging_station": "Laddstation som inte kan nås", 78 | "unreachable_zone": "Zon inte tillgänglig", 79 | "upside_down": "Gräsklipparen är upp och ner", 80 | "wheel_motor_blocked": "Hjulmotorn är blockerad", 81 | "wire_missing": "Begränsningskabel saknas", 82 | "wire_sync": "Trådsynkronisering" 83 | } 84 | } 85 | } 86 | }, 87 | "services": { 88 | "config": { 89 | "description": "Ställ enhets inställning", 90 | "fields": { 91 | "multizone_distances": { 92 | "description": "Ställ multizone avstånd i meter. 0=Av. Fortmat: 15, 80, 120, 155", 93 | "name": "Multi zon avstånd" 94 | }, 95 | "multizone_probabilities": { 96 | "description": "Ställ multizon sannolikhets upställning. Format: 50, 10, 20, 20", 97 | "name": "Multizon sannolikheter" 98 | }, 99 | "raindelay": { 100 | "description": "Ställ in regnfördröjning. Tid i minuter från 0 till 300. 0 = Inaktiverad", 101 | "name": "Regnfördröjning" 102 | }, 103 | "timeextension": { 104 | "description": "Ställ in förlängning av arbetstiden. Förlängning i % från -100 till 100", 105 | "name": "Förlängning av arbetstiden." 106 | } 107 | }, 108 | "name": "Ställ zon" 109 | }, 110 | "edgecut": { 111 | "description": "Starta kantklippning (om det stöds)", 112 | "name": "Kantklippning" 113 | }, 114 | "ots": { 115 | "description": "Starta en gångs schema ( Om stöttad )", 116 | "fields": { 117 | "boundary": { 118 | "description": "Klipp gränsen", 119 | "name": "Gräns" 120 | }, 121 | "runtime": { 122 | "description": "Körtid i minuter innan återvänder till laddstationen", 123 | "name": "Kör tid" 124 | } 125 | }, 126 | "name": "Engångsschema" 127 | }, 128 | "restart": { 129 | "description": "Startar om enheten", 130 | "name": "Starta om enheten" 131 | }, 132 | "schedule": { 133 | "description": "Ställ eller ändra schema för klipparen", 134 | "fields": { 135 | "friday_boundary": { 136 | "description": "Ska vi börja detta schema med att skära gränsen (kant/gränssnitt)?", 137 | "name": "Fredags gräns" 138 | }, 139 | "friday_end": { 140 | "description": "När ska schemat sluta på fredagar?", 141 | "name": "Fredag, Slut" 142 | }, 143 | "friday_start": { 144 | "description": "Starttid för fredagar", 145 | "name": "Fredag, Start" 146 | }, 147 | "monday_boundary": { 148 | "description": "Ska vi börja detta schema genom att skära gränsen (kant-/gränssnitt)?", 149 | "name": "Måndag, Gräns" 150 | }, 151 | "monday_end": { 152 | "description": "När ska schemat sluta på måndagar?", 153 | "name": "Måndag, Slut" 154 | }, 155 | "monday_start": { 156 | "description": "Starttid för måndagar", 157 | "name": "Måndag, Start" 158 | }, 159 | "saturday_boundary": { 160 | "description": "Ska vi starta schemat med att klippa gränsen?", 161 | "name": "Lördags, Gräns" 162 | }, 163 | "saturday_end": { 164 | "description": "När ska schemat sluta på Lördagar?", 165 | "name": "Lördags, Slut" 166 | }, 167 | "saturday_start": { 168 | "description": "Starttid för Lördagar", 169 | "name": "Lördag, Start" 170 | }, 171 | "sunday_boundary": { 172 | "description": "Ska vi starta schemat med att klippa gränsen?", 173 | "name": "Söndags gräns" 174 | }, 175 | "sunday_end": { 176 | "description": "När ska schemat sluta på söndagar?", 177 | "name": "Söndag, Slut" 178 | }, 179 | "sunday_start": { 180 | "description": "Starttid för Söndagar", 181 | "name": "Söndag, Start" 182 | }, 183 | "thursday_boundary": { 184 | "description": "Ska vi börja detta schema med att skära gränsen (kant/gränssnitt)?", 185 | "name": "Torsdag, Gräns" 186 | }, 187 | "thursday_end": { 188 | "description": "När ska schemat sluta på torsdagar?", 189 | "name": "Torsdag, Slut" 190 | }, 191 | "thursday_start": { 192 | "description": "Starttid för torsdagar", 193 | "name": "Torsdag, Start" 194 | }, 195 | "tuesday_boundary": { 196 | "description": "Ska vi börja detta schema genom att skära gränsen (kant-/gränssnitt)?", 197 | "name": "Tisdag, Gräns" 198 | }, 199 | "tuesday_end": { 200 | "description": "När ska schemat sluta på tisdagar?", 201 | "name": "Tisdag, Slut" 202 | }, 203 | "tuesday_start": { 204 | "description": "Starttid för tisdagar", 205 | "name": "Tisdag, Start" 206 | }, 207 | "type": { 208 | "description": "Ändra primär eller sekundär schema?", 209 | "name": "Schematyp" 210 | }, 211 | "wednesday_boundary": { 212 | "description": "Ska vi börja detta schema genom att skära gränsen (kant-/gränssnitt)?", 213 | "name": "Onsdag, Gräns" 214 | }, 215 | "wednesday_end": { 216 | "description": "När ska schemat sluta på onsdagar?", 217 | "name": "Onsdag, Slut" 218 | }, 219 | "wednesday_start": { 220 | "description": "Starttid för onsdagar", 221 | "name": "Onsdag, Start" 222 | } 223 | }, 224 | "name": "Ställ eller uppdatera schema" 225 | }, 226 | "send_raw": { 227 | "description": "Skicka ett JSON kommando till enheten", 228 | "fields": { 229 | "json": { 230 | "description": "Skicka data, Korrekt JSON format", 231 | "name": "JSON information" 232 | } 233 | }, 234 | "name": "Skicka RAW kommando" 235 | }, 236 | "setzone": { 237 | "description": "Ställ vilken zon som ska bli klippt härnäst", 238 | "fields": { 239 | "zone": { 240 | "description": "Sätt zon nummer, från 0 till 3, som ska bli klippt härnäst", 241 | "name": "Zon" 242 | } 243 | }, 244 | "name": "Sätt zon" 245 | }, 246 | "torque": { 247 | "description": "Ställ in hjulens vridmoment (om det stöds)", 248 | "fields": { 249 | "torque": { 250 | "description": "Ställ in hjulens vridmoment. Från -50 % till 50 %", 251 | "name": "Hjulens vridmoment" 252 | } 253 | }, 254 | "name": "Vridmoment" 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Ez a felhasználói fiók már be van állítva" 5 | }, 6 | "error": { 7 | "cannot_connect": "Kapcsolódási hiba", 8 | "invalid_auth": "Hitelesítési hiba", 9 | "too_many_requests": "Túl sok API hívás. Próbálja meg 24 óra múlva", 10 | "unknown": "Váratlan hiba" 11 | }, 12 | "step": { 13 | "user": { 14 | "data": { 15 | "email": "E-mail", 16 | "password": "Jelszó", 17 | "type": "Létrehoz" 18 | }, 19 | "title": "Csatlakozás a Landroid Cloud fiókhoz" 20 | } 21 | } 22 | }, 23 | "entity": { 24 | "lawn_mower": { 25 | "landroid_cloud": { 26 | "state": { 27 | "edgecut": "Szélvágás", 28 | "escaped_digital_fence": "A digitális kerítésen kívül", 29 | "idle": "Tétlen", 30 | "initializing": "Inicializálás", 31 | "locked": "Lezárva", 32 | "mowing": "Fűvágás", 33 | "offline": "Offline", 34 | "rain_delay": "Eső késleltetés", 35 | "returning": "Visszatérés a töltőhöz", 36 | "searching_zone": "Zóna keresése", 37 | "starting": "Indulás", 38 | "unknown": "Ismeretlen", 39 | "zoning": "Zóna betanítás" 40 | } 41 | } 42 | }, 43 | "sensor": { 44 | "landroid_cloud_error": { 45 | "state": { 46 | "battery_low": "Az akkumulátor lemerült", 47 | "battery_temperature_error": "Akkumulátor hőmérsékleti hiba", 48 | "battery_trunk_open_timeout": "Az akkutartó túl sokáig van nyitva", 49 | "blade_height_adjustment_blocked": "A penge magasság állítása meghiúsult.", 50 | "blade_motor_blocked": "A vágómotor blokkolva van", 51 | "camera_error": "Kamera hiba", 52 | "charge_error": "Töltési hiba", 53 | "charging_station_docking_error": "Töltőállomás dokkolási hiba", 54 | "close_door_to_go_home": "Csukja be az ajtót a haza induláshoz", 55 | "close_door_to_mow": "Csukja be az ajtót a fűnyíráshoz", 56 | "excessive_slope": "A lejtő túl meredek", 57 | "hbi_error": "HBI hiba", 58 | "headlight_error": "Fényszóró hiba", 59 | "insufficient_sensor_data": "Nincs elegendő érzékelő adat", 60 | "lifted": "Felemelve", 61 | "locked": "A fűnyíró le van zárva", 62 | "map_error": "Térképhiba", 63 | "mapping_exploration_failed": "Térképfelderítés sikertelen", 64 | "mapping_exploration_required": "Térképfelderítés szükséges", 65 | "missing_charging_station": "A töltőállomás hiányzik", 66 | "no_error": "Nincs hiba", 67 | "ota_error": "OTA hiba", 68 | "outside_wire": "Vezetődróton kívül", 69 | "rain_delay": "Az eső késleltetése aktív", 70 | "reverse_wire": "A vezetődrót fodított irányú", 71 | "rfid_reader_error": "RFID olvasó hiba", 72 | "timeout_finding_home": "Időtúllépés dokkoló keresése közben", 73 | "training_start_disallowed": "A betanítás elindítása nem engedélyezett", 74 | "trapped": "Megakadt", 75 | "trapped_timeout": "Időtúllépés megakadás közben", 76 | "unknown": "Ismeretlen", 77 | "unreachable_charging_station": "A töltőállomás nem elérhető", 78 | "unreachable_zone": "Elérhetetlen zóna", 79 | "upside_down": "A fűnyíró fejjel lefelé van", 80 | "wheel_motor_blocked": "A kerékmotor blokkolva van", 81 | "wire_missing": "Hiányzó vezetődrót", 82 | "wire_sync": "Vezetődrót szinkronizálás" 83 | } 84 | } 85 | } 86 | }, 87 | "services": { 88 | "config": { 89 | "description": "Eszközkonfigurációs paraméterek beállítása", 90 | "fields": { 91 | "multizone_distances": { 92 | "description": "Zóna kezdési távolságok megadása méterben. 0=kikapcsol, Formátum: 15, 80, 120, 155", 93 | "name": "Zóna kezdési távolságok" 94 | }, 95 | "multizone_probabilities": { 96 | "description": "Zónák közötti arányok beállítása. Formátum: 50, 10, 20, 20", 97 | "name": "Zónák közötti arányok" 98 | }, 99 | "raindelay": { 100 | "description": "Az eső késleltetés beállítása percekben 0 és 300 között. 0 = kikapcsolva", 101 | "name": "Eső késleltetés" 102 | }, 103 | "timeextension": { 104 | "description": "A vágási idő hosszabbítása -100% és +100% között", 105 | "name": "Időhosszabbítás" 106 | } 107 | }, 108 | "name": "Zóna beállítása" 109 | }, 110 | "edgecut": { 111 | "description": "Szélvágás indítása (ha támogatott)", 112 | "name": "Szélvágás" 113 | }, 114 | "ots": { 115 | "description": "Egyszeri indítás (ha támogatott)", 116 | "fields": { 117 | "boundary": { 118 | "description": "Végezzen szélvágást", 119 | "name": "Szélvágás" 120 | }, 121 | "runtime": { 122 | "description": "A töltőállomásra visszatérésig eltelt futási idő", 123 | "name": "Futási idő" 124 | } 125 | }, 126 | "name": "Egyszeri indítás" 127 | }, 128 | "restart": { 129 | "description": "Újraindítja az eszközt", 130 | "name": "Eszköz újraindítása" 131 | }, 132 | "schedule": { 133 | "description": "Állítsa be vagy módosítsa a fűnyíró ütemezését", 134 | "fields": { 135 | "friday_boundary": { 136 | "description": "Az ütemezés előtt legyen szélvágás?", 137 | "name": "Péntek, szélvágás" 138 | }, 139 | "friday_end": { 140 | "description": "Mikor érjen véget az ütemezés péntekenként?", 141 | "name": "Péntek, vége" 142 | }, 143 | "friday_start": { 144 | "description": "Ütemezés kezdete péntekenként", 145 | "name": "Péntek, kezdés" 146 | }, 147 | "monday_boundary": { 148 | "description": "Az ütemezés előtt legyen szélvágás?", 149 | "name": "Hétfő, szélvágás" 150 | }, 151 | "monday_end": { 152 | "description": "Mikor érjen véget az ütemezés hétfőnként?", 153 | "name": "Hétfő, vége" 154 | }, 155 | "monday_start": { 156 | "description": "Ütemezés kezdete hétfőnként", 157 | "name": "Hétfő, kezdés" 158 | }, 159 | "saturday_boundary": { 160 | "description": "Az ütemezés előtt legyen szélvágás?", 161 | "name": "Szombat, szélvágás" 162 | }, 163 | "saturday_end": { 164 | "description": "Mikor érjen véget az ütemezés szombatonként?", 165 | "name": "szombat, vége" 166 | }, 167 | "saturday_start": { 168 | "description": "Ütemezés kezdete szombatonként", 169 | "name": "Szombat, kezdés" 170 | }, 171 | "sunday_boundary": { 172 | "description": "Az ütemezés előtt legyen szélvágás?", 173 | "name": "Vasárnap, szélvágáa" 174 | }, 175 | "sunday_end": { 176 | "description": "Mikor érjen véget az ütemezés vasárnaponként?", 177 | "name": "Vasárnap, vége" 178 | }, 179 | "sunday_start": { 180 | "description": "Ütemezés kezdete vasárnaponként", 181 | "name": "Vasárnap, kezdés" 182 | }, 183 | "thursday_boundary": { 184 | "description": "Az ütemezés előtt legyen szélvágás?", 185 | "name": "Csütörtök, szélvágás" 186 | }, 187 | "thursday_end": { 188 | "description": "Mikor érjen véget az ütemezés csütörtökönként?", 189 | "name": "Csütörtök, vége" 190 | }, 191 | "thursday_start": { 192 | "description": "Ütemezés kezdete csütörtökönként", 193 | "name": "Csütörtök, kezdés" 194 | }, 195 | "tuesday_boundary": { 196 | "description": "Az ütemezés előtt legyen szélvágás?", 197 | "name": "Kedd, szélvágás" 198 | }, 199 | "tuesday_end": { 200 | "description": "Mikor érjen véget az ütemezés keddenként?", 201 | "name": "Kedd, vége" 202 | }, 203 | "tuesday_start": { 204 | "description": "Ütemezés kezdete keddenként", 205 | "name": "Kedd, kezdés" 206 | }, 207 | "type": { 208 | "description": "Módosítja az elsődleges vagy másodlagos ütemezést?", 209 | "name": "Ütemezés típusa" 210 | }, 211 | "wednesday_boundary": { 212 | "description": "Az ütemezés előtt legyen szélvágás?", 213 | "name": "Szerda, szélvágás" 214 | }, 215 | "wednesday_end": { 216 | "description": "Mikor érjen véget az ütemezés szerdánként?", 217 | "name": "Szerda, vége" 218 | }, 219 | "wednesday_start": { 220 | "description": "Ütemezés kezdete szerdánként", 221 | "name": "Szerda, kezdés" 222 | } 223 | }, 224 | "name": "Ütemezés beállítása vagy frissítése" 225 | }, 226 | "send_raw": { 227 | "description": "Nyers JSON-parancs küldése az eszközre", 228 | "fields": { 229 | "json": { 230 | "description": "Elküldendő adatok, érvényes JSON formátumban", 231 | "name": "JSON adat" 232 | } 233 | }, 234 | "name": "Nyers parancs küldése" 235 | }, 236 | "setzone": { 237 | "description": "A következő zóna megadása", 238 | "fields": { 239 | "zone": { 240 | "description": "A következő vágási zóna számának megadása 0 és 3 között", 241 | "name": "Zóna" 242 | } 243 | }, 244 | "name": "Zóna megadása" 245 | }, 246 | "torque": { 247 | "description": "Keréknyomaték beállítása. (ha támogatott)", 248 | "fields": { 249 | "torque": { 250 | "description": "Keréknyomaték beálltása -50% és +50% között.", 251 | "name": "Keréknyomaték" 252 | } 253 | }, 254 | "name": "Nyomaték" 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "Tento uživatelský účet již byl nastaven" 5 | }, 6 | "error": { 7 | "cannot_connect": "Chyba připojení", 8 | "invalid_auth": "Chyba přihlášení", 9 | "too_many_requests": "Příliš mnoho požadavků na API – Zkuste to znovu za 24 hodin.", 10 | "unknown": "Neočekávaná chyba" 11 | }, 12 | "step": { 13 | "user": { 14 | "data": { 15 | "email": "Email", 16 | "password": "Heslo", 17 | "type": "Model" 18 | }, 19 | "title": "Přihlášení k Landroid Cloud účtu" 20 | } 21 | } 22 | }, 23 | "entity": { 24 | "lawn_mower": { 25 | "landroid_cloud": { 26 | "state": { 27 | "edgecut": "Sekání podél okraje", 28 | "escaped_digital_fence": "Sekačka překročila virtuální plot", 29 | "idle": "Nečinný", 30 | "initializing": "Inicializace", 31 | "locked": "Uzamknuto", 32 | "mowing": "Sekání", 33 | "offline": "Offline", 34 | "rain_delay": "Prodleva po dešti", 35 | "returning": "Návrat k nabíjecí stanici", 36 | "searching_zone": "Vyhledávání zóny", 37 | "starting": "Začínání", 38 | "unknown": "Neznámý", 39 | "zoning": "Učení zóny" 40 | } 41 | } 42 | }, 43 | "sensor": { 44 | "landroid_cloud_error": { 45 | "state": { 46 | "battery_low": "Vybitá baterie", 47 | "battery_temperature_error": "Chyba teploty baterie", 48 | "battery_trunk_open_timeout": "Vypršel časový limit otevření přihrádky na baterii", 49 | "blade_height_adjustment_blocked": "Zablokované nastavení výšky nožů", 50 | "blade_motor_blocked": "Nože jsou zablokovány", 51 | "camera_error": "Chyba kamery", 52 | "charge_error": "Chyba nabíjení", 53 | "charging_station_docking_error": "Chyba dokování nabíjecí stanice", 54 | "close_door_to_go_home": "Zavřete dvířka pro návrat do doku", 55 | "close_door_to_mow": "Zavřete dvířka pro sekání", 56 | "excessive_slope": "Zjištěn nadměrný sklon", 57 | "hbi_error": "Chyba HBI (rozpoznání okraje)", 58 | "headlight_error": "Chyba světlometu", 59 | "insufficient_sensor_data": "Nedostatek dat ze senzorů", 60 | "lifted": "Zvednuto", 61 | "locked": "Sekačka je uzamčena", 62 | "map_error": "Chyba mapy", 63 | "mapping_exploration_failed": "Průzkumné mapování selhalo", 64 | "mapping_exploration_required": "Je vyžadováno průzkumné mapování", 65 | "missing_charging_station": "Chybí nabíjecí stanice", 66 | "no_error": "Žádná chyba", 67 | "ota_error": "Chyba OTA", 68 | "outside_wire": "Mimo sekací prostor", 69 | "rain_delay": "Zpoždění po dešti je aktivní", 70 | "reverse_wire": "Drát je obrácený", 71 | "rfid_reader_error": "Chyba čtečky RFID", 72 | "timeout_finding_home": "Vypršel časový limit při hledání doku", 73 | "training_start_disallowed": "Spuštění učení nepovoleno", 74 | "trapped": "Uváznutí", 75 | "trapped_timeout": "Vypršel časový limit při uváznutí", 76 | "unknown": "Neznámý", 77 | "unreachable_charging_station": "Nedosažitelná nabíjecí stanice", 78 | "unreachable_zone": "Nedosažitelná zóna", 79 | "upside_down": "Sekačka je vzhůru nohama", 80 | "wheel_motor_blocked": "Pohon kol je zablokován", 81 | "wire_missing": "Chybí ohraničující drát", 82 | "wire_sync": "Synchronizace kabelem" 83 | } 84 | } 85 | } 86 | }, 87 | "services": { 88 | "config": { 89 | "description": "Nastavení konfiguračních parametrů zařízení", 90 | "fields": { 91 | "multizone_distances": { 92 | "description": "Nastavení pole vzdáleností pro více zón. V metrech. 0 = Zakázáno. Formát: 15, 80, 120, 155", 93 | "name": "Vzdálenosti ve více zónách" 94 | }, 95 | "multizone_probabilities": { 96 | "description": "Nastavení pole pravděpodobností pro více zón. Formát: 50, 10, 20, 20", 97 | "name": "Pravděpodobnosti pro více zón" 98 | }, 99 | "raindelay": { 100 | "description": "Nastavení délky prodlevy po dešti. Čas v minutách od 0 do 300. 0 = Zakázáno", 101 | "name": "Prodleva po dešti" 102 | }, 103 | "timeextension": { 104 | "description": "Nastavení prodloužení času. Prodloužení v % v rozmezí od -100 do 100", 105 | "name": "Prodloužení času" 106 | } 107 | }, 108 | "name": "Nastavit zónu" 109 | }, 110 | "edgecut": { 111 | "description": "Spustit sekání okraje (je-li podporováno)", 112 | "name": "Sekání okraje" 113 | }, 114 | "ots": { 115 | "description": "Spuštění jednorázového harmonogramu (je-li podporován)", 116 | "fields": { 117 | "boundary": { 118 | "description": "Sekání okraje", 119 | "name": "Okraj" 120 | }, 121 | "runtime": { 122 | "description": "Doba provozu v minutách před návratem do nabíjecí stanice", 123 | "name": "Doba provozu" 124 | } 125 | }, 126 | "name": "Jednorázový harmonogram" 127 | }, 128 | "restart": { 129 | "description": "Restartuje zařízení", 130 | "name": "Restart zařízení" 131 | }, 132 | "schedule": { 133 | "description": "Nastavení nebo změna časového plánu sekačky", 134 | "fields": { 135 | "friday_boundary": { 136 | "description": "Má páteční plán začít sekáním okraje?", 137 | "name": "Pátek, sekání okraje" 138 | }, 139 | "friday_end": { 140 | "description": "Kdy má páteční plán skončit?", 141 | "name": "Pátek, konec" 142 | }, 143 | "friday_start": { 144 | "description": "Kdy má páteční plán začít?", 145 | "name": "Pátek, začátek" 146 | }, 147 | "monday_boundary": { 148 | "description": "Má pondělní plán začít sekáním okraje?", 149 | "name": "Pondělí, sekání okraje" 150 | }, 151 | "monday_end": { 152 | "description": "Kdy má pondělní plán skončit?", 153 | "name": "Pondělí, konec" 154 | }, 155 | "monday_start": { 156 | "description": "Kdy má pondělní plán začít?", 157 | "name": "Pondělí, začátek" 158 | }, 159 | "saturday_boundary": { 160 | "description": "Má sobotní plán začít sekáním okraje?", 161 | "name": "Sobota, sekání okraje" 162 | }, 163 | "saturday_end": { 164 | "description": "Kdy má sobotní plán skončit?", 165 | "name": "Sobota, konec" 166 | }, 167 | "saturday_start": { 168 | "description": "Kdy má sobotní plán začít?", 169 | "name": "Sobota, začátek" 170 | }, 171 | "sunday_boundary": { 172 | "description": "Má nedělní plán začít sekáním okraje?", 173 | "name": "Neděle, sekání okraje" 174 | }, 175 | "sunday_end": { 176 | "description": "Kdy má nedělní plán skončit?", 177 | "name": "Neděle, konec" 178 | }, 179 | "sunday_start": { 180 | "description": "Kdy má nedělní plán začít?", 181 | "name": "Neděle, začátek" 182 | }, 183 | "thursday_boundary": { 184 | "description": "Má čtvrteční plán začít sekáním okraje?", 185 | "name": "Čtvrtek, sekání okraje" 186 | }, 187 | "thursday_end": { 188 | "description": "Kdy má čtvrteční plán skončit?", 189 | "name": "Čtvrtek, konec" 190 | }, 191 | "thursday_start": { 192 | "description": "Kdy má čtvrteční plán začít?", 193 | "name": "Čtvrtek, začátek" 194 | }, 195 | "tuesday_boundary": { 196 | "description": "Má úterní plán začít sekáním okraje?", 197 | "name": "Úterý, sekání okraje" 198 | }, 199 | "tuesday_end": { 200 | "description": "Kdy má úterní plán skončit?", 201 | "name": "Úterý, konec" 202 | }, 203 | "tuesday_start": { 204 | "description": "Kdy má úterní plán začít?", 205 | "name": "Úterý, začátek" 206 | }, 207 | "type": { 208 | "description": "Změna primárního nebo sekundárního plánu?", 209 | "name": "Typ plánu" 210 | }, 211 | "wednesday_boundary": { 212 | "description": "Má středeční plán začít sekáním okraje?", 213 | "name": "Středa, sekání okraje" 214 | }, 215 | "wednesday_end": { 216 | "description": "Kdy má středeční plán skončit?", 217 | "name": "Středa, konec" 218 | }, 219 | "wednesday_start": { 220 | "description": "Kdy má středeční plán začít?", 221 | "name": "Středa, začátek" 222 | } 223 | }, 224 | "name": "Nastavení nebo aktualizace plánu" 225 | }, 226 | "send_raw": { 227 | "description": "Odeslat nezpracovaný JSON příkaz", 228 | "fields": { 229 | "json": { 230 | "description": "Data k odeslání, formátovaná jako platný JSON", 231 | "name": "JSON data" 232 | } 233 | }, 234 | "name": "Odeslat nezpracovaný (RAW) příkaz" 235 | }, 236 | "setzone": { 237 | "description": "Nastavení zóny, která se má sekat příště", 238 | "fields": { 239 | "zone": { 240 | "description": "Nastavení čísla zóny (od 0 do 3), která se bude sekat příště.", 241 | "name": "Zóna" 242 | } 243 | }, 244 | "name": "Nastavení zóny" 245 | }, 246 | "torque": { 247 | "description": "Nastavení síly pohonu (je-li podporováno)", 248 | "fields": { 249 | "torque": { 250 | "description": "Nastavení síly pohonu. V rozmezí od -50 % do 50 %", 251 | "name": "Síla pohonu" 252 | } 253 | }, 254 | "name": "Síla" 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /custom_components/landroid_cloud/translations/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_exists": "See kasutajakonto on juba konfigureeritud" 5 | }, 6 | "error": { 7 | "cannot_connect": "Ühendamine ebaõnnestus", 8 | "invalid_auth": "Viga autentimisel", 9 | "too_many_requests": "Liiga palju päringuid API-le - proovige uuesti 24 tunni pärast.", 10 | "unknown": "Ootamatu viga" 11 | }, 12 | "step": { 13 | "user": { 14 | "data": { 15 | "email": "E-post", 16 | "password": "Salasõna", 17 | "type": "Tee" 18 | }, 19 | "title": "Looge ühendus oma Landroid Cloudi kontoga" 20 | } 21 | } 22 | }, 23 | "entity": { 24 | "lawn_mower": { 25 | "landroid_cloud": { 26 | "state": { 27 | "edgecut": "Äärelõikus", 28 | "escaped_digital_fence": "Digitaalselt piirdest välja pääsenud", 29 | "initializing": "Initsialiseerimine", 30 | "mowing": "Niitmine", 31 | "offline": "Ühenduseta", 32 | "rain_delay": "Vihma viivitus", 33 | "returning": "Laadija juurde tagasi pöördumine", 34 | "searching_zone": "Tsooni otsimine", 35 | "starting": "Käivitamine", 36 | "unknown": "Tundmatu", 37 | "zoning": "Tsooni treening" 38 | } 39 | } 40 | }, 41 | "sensor": { 42 | "landroid_cloud_error": { 43 | "state": { 44 | "battery_low": "Aku tühi", 45 | "battery_temperature_error": "Aku temperatuuri viga", 46 | "battery_trunk_open_timeout": "Aku pagasiruumi avamise aegumine", 47 | "blade_height_adjustment_blocked": "Tera kõrguse reguleerimine blokeeritud", 48 | "blade_motor_blocked": "Tera mootor on blokeeritud", 49 | "camera_error": "Kaamera viga", 50 | "charge_error": "Viga laadimisel", 51 | "charging_station_docking_error": "Laadimisjaama dokkimisviga", 52 | "close_door_to_go_home": "Sulge uks, et naasta koju", 53 | "close_door_to_mow": "Niitmiseks sulgege uks", 54 | "excessive_slope": "Tuvastati liigne kalle", 55 | "hbi_error": "HBI viga", 56 | "headlight_error": "Esitulede viga", 57 | "insufficient_sensor_data": "Anduri andmed on ebapiisavad", 58 | "lifted": "Ülestõstetud", 59 | "locked": "Niiduk on lukustatud", 60 | "map_error": "Kaardi viga", 61 | "mapping_exploration_failed": "Kaardistamine ebaõnnestus", 62 | "mapping_exploration_required": "Kaardistamine on vajalik", 63 | "missing_charging_station": "Laadimisjaam on kättesaamatu", 64 | "no_error": "Vigu pole", 65 | "ota_error": "OTA viga", 66 | "outside_wire": "Väljaspool piirdetraati", 67 | "rain_delay": "Vihma viivitus aktiivne", 68 | "reverse_wire": "Piirdetraat on vastupidine", 69 | "rfid_reader_error": "RFID-lugeja viga", 70 | "timeout_finding_home": "Aegus kodu leidmisel", 71 | "training_start_disallowed": "Treeningu alustamine on keelatud", 72 | "trapped": "Lõksus", 73 | "trapped_timeout": "Aegunud kinni olles", 74 | "unknown": "Tundmatu", 75 | "unreachable_charging_station": "Laadimisjaam on kättesaamatu", 76 | "unreachable_zone": "Tsoon läbipääsmatu", 77 | "upside_down": "Niiduk on tagurpidi", 78 | "wheel_motor_blocked": "Ratta mootor on blokeeritud", 79 | "wire_missing": "Piirdetraat kadunud", 80 | "wire_sync": "Piirdekaabli sünkroniseerimine" 81 | } 82 | } 83 | } 84 | }, 85 | "services": { 86 | "config": { 87 | "description": "Seadistage seadme konfiguratsiooniparameetrid", 88 | "fields": { 89 | "multizone_distances": { 90 | "description": "Mitmetsoonilise kauguse määramine meetrites. 0 = keelatud. Vorming: 15, 80, 120, 155", 91 | "name": "Mitme tsooni vahemaad" 92 | }, 93 | "multizone_probabilities": { 94 | "description": "Mitme tsooni tõenäosuste määramine. Formaat: 50, 10, 20, 20", 95 | "name": "Mitme tsooni tõenäosused" 96 | }, 97 | "raindelay": { 98 | "description": "Määra vihma viivitus. Aeg minutites vahemikus 0 kuni 300. 0 = keelatud", 99 | "name": "Vihma viivitus" 100 | }, 101 | "timeextension": { 102 | "description": "Määra ajapikendus. Pikendamine protsentides vahemikus -100 kuni 100", 103 | "name": "Ajapikendus" 104 | } 105 | }, 106 | "name": "Määra tsoon" 107 | }, 108 | "edgecut": { 109 | "description": "Alusta servalõikust (kui seda toetatakse)", 110 | "name": "Ääre-/servalõige" 111 | }, 112 | "ots": { 113 | "description": "Ühekordse ajakava käivitamine (kui see on toetatud)", 114 | "fields": { 115 | "boundary": { 116 | "description": "Niida piiri (ääre/serva lõikamine)", 117 | "name": "Piirid" 118 | }, 119 | "runtime": { 120 | "description": "Tööaeg minutites enne laadimisjaama naasmist", 121 | "name": "Tööaeg" 122 | } 123 | }, 124 | "name": "Ühekordne ajakava" 125 | }, 126 | "restart": { 127 | "description": "Taaskäivitab seadme", 128 | "name": "Seadme taaskäivitamine" 129 | }, 130 | "schedule": { 131 | "description": "Niiduki ajakava seadmine või muutmine", 132 | "fields": { 133 | "friday_boundary": { 134 | "description": "Kas me peaksime alustama seda ajakava piiri lõikamisega (serva/piiri lõikamine)?", 135 | "name": "Reede, piir" 136 | }, 137 | "friday_end": { 138 | "description": "Millal peaks ajakava reedeti lõppema?", 139 | "name": "Reede, lõpp" 140 | }, 141 | "friday_start": { 142 | "description": "Algusaeg reedeti", 143 | "name": "Reede, algus" 144 | }, 145 | "monday_boundary": { 146 | "description": "Kas peaksime seda graafikut alustama piiri lõikamisega (serva/piiri lõikamine)?", 147 | "name": "Esmaspäev, piir" 148 | }, 149 | "monday_end": { 150 | "description": "Millal peaks graafik esmaspäeviti lõppema?", 151 | "name": "Esmaspäev, lõpp" 152 | }, 153 | "monday_start": { 154 | "description": "Algusaeg esmaspäeviti", 155 | "name": "Esmaspäev, algus" 156 | }, 157 | "saturday_boundary": { 158 | "description": "Kas me peaksime alustama seda ajakava piiri lõikamisega (serva/piiri lõikamine)?", 159 | "name": "Laupäev, piir" 160 | }, 161 | "saturday_end": { 162 | "description": "Millal peaks ajakava laupäeviti lõppema?", 163 | "name": "Laupäev, lõpp" 164 | }, 165 | "saturday_start": { 166 | "description": "Algusaeg laupäeviti", 167 | "name": "Laupäev, algus" 168 | }, 169 | "sunday_boundary": { 170 | "description": "Kas me peaksime alustama seda ajakava piiri lõikamisega (serva/piiri lõikamine)?", 171 | "name": "Pühapäev, piir" 172 | }, 173 | "sunday_end": { 174 | "description": "Millal peaks graafik pühapäeviti lõppema?", 175 | "name": "Pühapäev, lõpp" 176 | }, 177 | "sunday_start": { 178 | "description": "Algusaeg pühapäeviti", 179 | "name": "Pühapäev, algus" 180 | }, 181 | "thursday_boundary": { 182 | "description": "Kas me peaksime alustama seda ajakava piiri lõikamisega (serva/piiri lõikamine)?", 183 | "name": "Neljapäev, piir" 184 | }, 185 | "thursday_end": { 186 | "description": "Millal peaks ajakava neljapäeviti lõppema?", 187 | "name": "Neljapäev, lõpp" 188 | }, 189 | "thursday_start": { 190 | "description": "Algusaeg neljapäeviti", 191 | "name": "Neljapäev, algus" 192 | }, 193 | "tuesday_boundary": { 194 | "description": "Kas peaksime seda graafikut alustama piiri lõikamisega (serva/piiri lõikamine)?", 195 | "name": "Teisipäev, piir" 196 | }, 197 | "tuesday_end": { 198 | "description": "Millal peaks graafik teisipäeviti lõppema?", 199 | "name": "Teisipäev, lõpp" 200 | }, 201 | "tuesday_start": { 202 | "description": "Algusaeg teisipäeviti", 203 | "name": "Teisipäev, algus" 204 | }, 205 | "type": { 206 | "description": "Kas muuta esmast või teisest ajakava?", 207 | "name": "Ajakava tüüp" 208 | }, 209 | "wednesday_boundary": { 210 | "description": "Kas peaksime seda graafikut alustama piiri lõikamisega (serva/piiri lõikamine)?", 211 | "name": "Kolmapäev, piir" 212 | }, 213 | "wednesday_end": { 214 | "description": "Millal peaks ajakava kolmapäeviti lõppema?", 215 | "name": "Kolmapäev, lõpp" 216 | }, 217 | "wednesday_start": { 218 | "description": "Algusaeg kolmapäeviti", 219 | "name": "Kolmapäev, algus" 220 | } 221 | }, 222 | "name": "Määrake või ajakohastage ajakava" 223 | }, 224 | "send_raw": { 225 | "description": "Saatke seadmele töötlemata JSON käsk", 226 | "fields": { 227 | "json": { 228 | "description": "Saadetavad andmed, mis on vormindatud kehtiva JSON-ina", 229 | "name": "JSON andmed" 230 | } 231 | }, 232 | "name": "RAW käsu saatmine" 233 | }, 234 | "setzone": { 235 | "description": "Määrake, millist tsooni järgmisena niita", 236 | "fields": { 237 | "zone": { 238 | "description": "Määrab tsooni numbri vahemikus 0 kuni 3, mida järgmisena niidetakse", 239 | "name": "Tsoon" 240 | } 241 | }, 242 | "name": "Määra tsoon" 243 | }, 244 | "torque": { 245 | "description": "Seadistage ratta pöördemoment (kui see on toetatud)", 246 | "fields": { 247 | "torque": { 248 | "description": "Seadke ratta pöördemoment. vahemikus -50% kuni 50%", 249 | "name": "Ratta pöördemoment" 250 | } 251 | }, 252 | "name": "Pöördemoment" 253 | } 254 | } 255 | } --------------------------------------------------------------------------------