├── .flake8 ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── unsupported_device.yaml ├── pr-labeler.yml ├── release-drafter.yml └── workflows │ ├── hassfest.yml │ ├── linters.yml │ ├── matchers │ └── python.json │ ├── pr-labeler.yml │ ├── release-drafter.yml │ ├── stale.yml │ └── workflow_updater.yaml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .hadolint.yaml ├── .isort.cfg ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .trunk ├── .gitignore └── trunk.yaml ├── .yamllint ├── README.md ├── custom_components └── vesync │ ├── __init__.py │ ├── binary_sensor.py │ ├── button.py │ ├── common.py │ ├── config_flow.py │ ├── const.py │ ├── device_action.py │ ├── diagnostics.py │ ├── fan.py │ ├── humidifier.py │ ├── light.py │ ├── manifest.json │ ├── number.py │ ├── sensor.py │ ├── services.yaml │ ├── strings.json │ ├── switch.py │ └── translations │ ├── bg.json │ ├── ca.json │ ├── cs.json │ ├── da.json │ ├── de.json │ ├── el.json │ ├── en.json │ ├── es-419.json │ ├── es.json │ ├── et.json │ ├── fr.json │ ├── he.json │ ├── hu.json │ ├── id.json │ ├── it.json │ ├── ja.json │ ├── ko.json │ ├── lb.json │ ├── lv.json │ ├── nl.json │ ├── no.json │ ├── pl.json │ ├── pt-BR.json │ ├── pt.json │ ├── ru.json │ ├── sk.json │ ├── sl.json │ ├── sv.json │ ├── tr.json │ ├── uk.json │ ├── zh-Hans.json │ └── zh-Hant.json ├── hacs.json ├── license.txt ├── requirements.txt ├── requirements_dev.txt └── setup.cfg /.flake8: -------------------------------------------------------------------------------- 1 | # Autoformatter friendly flake8 config (all formatting rules disabled) 2 | [flake8] 3 | extend-ignore = E1, E2, E3, E501, W1, W2, W3, W5 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve the integration 3 | labels: [bug] 4 | body: 5 | 6 | - type: textarea 7 | validations: 8 | required: true 9 | attributes: 10 | label: The problem 11 | description: >- 12 | Describe the issue you are experiencing here to communicate to the 13 | maintainers. Tell us what you were trying to do and what happened. 14 | 15 | Provide a clear and concise description of what the problem is. What did you expect to happen? 16 | 17 | - type: markdown 18 | attributes: 19 | value: | 20 | ## Environment 21 | 22 | - type: input 23 | id: version 24 | validations: 25 | required: true 26 | attributes: 27 | label: What version of this integration has the issue? 28 | placeholder: 2.5.0 29 | description: > 30 | Can be found in the Configuration panel -> Info. 31 | 32 | - type: input 33 | id: ha_version 34 | validations: 35 | required: true 36 | attributes: 37 | label: What version of Home Assistant Core has the issue? 38 | placeholder: core- 39 | description: > 40 | Can be found in the Configuration panel -> Info. 41 | 42 | - type: markdown 43 | attributes: 44 | value: | 45 | ## Device 46 | 47 | - type: textarea 48 | id: vesync_diagnostics 49 | validations: 50 | required: true 51 | attributes: 52 | label: Diagnostics 53 | placeholder: "{}" 54 | description: > 55 | Can be found in the Configuration panel -> Integrations -> VeSync -> Diagnostics 56 | value: | 57 |
Diagnostics 58 | 59 | ```json 60 | Copy/paste diagnostics here between the starting and ending backticks. 61 | ``` 62 | 63 |
64 | - type: markdown 65 | attributes: 66 | value: | 67 | ## Details 68 | 69 | - type: textarea 70 | id: logs 71 | attributes: 72 | label: Home Assistant log 73 | description: Enable debug logging and paste your full log here. Don't forget to redact sensitive information. 74 | value: | 75 |
Logs 76 | 77 | ```py 78 | Copy/paste any log here, between the starting and ending backticks. 79 | ``` 80 | 81 |
82 | 83 | - type: textarea 84 | id: additional-information 85 | attributes: 86 | label: Additional information 87 | description: If you have any additional information for us, use the field below. Please note, you can attach screenshots or screen recordings here, by dragging and dropping files in the field below. 88 | 89 | - type: markdown 90 | attributes: 91 | value: | 92 | Thanks for taking the time to fill out this bug report! 93 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/unsupported_device.yaml: -------------------------------------------------------------------------------- 1 | name: Unsupported Device 2 | description: Let's have a look if we can support your device 3 | title: "Add support for ..." 4 | labels: [new-device] 5 | body: 6 | 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Device 11 | 12 | - type: textarea 13 | id: unsupported_device_log 14 | attributes: 15 | label: Home Assistant log 16 | description: Enable [debug logging](https://github.com/vlebourl/custom_vesync#enable-debug-logging) and paste your full log here. 17 | value: | 18 |
Logs 19 | 20 | ``` 21 | Copy/paste any log here, between the starting and ending backticks. The first log line must start with "Found the following devices:". 22 | ``` 23 | 24 |
25 | 26 | - type: markdown 27 | attributes: 28 | value: | 29 | ## Details 30 | 31 | - type: textarea 32 | id: additional-information 33 | attributes: 34 | label: Additional information 35 | description: If you have any additional information for us, use the field below. Please note, you can attach screenshots or screen recordings here, by dragging and dropping files in the field below. 36 | 37 | - type: markdown 38 | attributes: 39 | value: | 40 | Thanks for taking the time to fill out this request! 41 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | feature: ['feature/*', 'feat/*'] 2 | enhancement: enhancement/* 3 | bug: fix/* 4 | breaking: breaking/* 5 | documentation: doc/* 6 | backport: '*backport*' 7 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | exclude-labels: 4 | - 'Meta: Exclude From Changelog' 5 | - 'auto-changelog' 6 | categories: 7 | - title: '⤽ Backport from Core' 8 | label: 'backport' 9 | - title: '⚠️ Breaking changes' 10 | label: 'breaking' 11 | - title: '🚀 Features' 12 | label: 'feature' 13 | - title: '✨ Enhancement' 14 | label: 'enhancement' 15 | - title: '📘 Documentation' 16 | label: 'documentation' 17 | - title: '🐛 Bug Fixes' 18 | label: 'bug' 19 | template: | 20 | ## What's changed 21 | $CHANGES 22 | ## Contributors to this release 23 | $CONTRIBUTORS 24 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v3.6.0" 14 | - uses: home-assistant/actions/hassfest@master 15 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters (flake8, black, isort) 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4.1.0 11 | - uses: actions/setup-python@v4.7.0 12 | - uses: pre-commit/action@v3.0.0 13 | -------------------------------------------------------------------------------- /.github/workflows/matchers/python.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "python", 5 | "pattern": [ 6 | { 7 | "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", 8 | "file": 1, 9 | "line": 2 10 | }, 11 | { 12 | "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", 13 | "message": 2 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.github/workflows/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | jobs: 7 | pr-labeler: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: TimonVS/pr-labeler-action@v4.1.1 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Update release draft 13 | uses: release-drafter/release-drafter@v5.24.0 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/stale@v8.0.0 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: > 16 | 'There hasn't been any activity on this issue recently. Is this issue still present? 17 | 18 | Please make sure to update to the latest Home Assistant version and version of this integration to see 19 | if that solves the issue. Let us know if that works for you by adding a 20 | comment 👍. 21 | 22 | This issue now has been marked as stale and will be closed if no further 23 | activity occurs. Thank you for your contributions.' 24 | days-before-stale: 30 25 | days-before-close: 30 26 | stale-issue-label: 'no-issue-activity' 27 | exempt-issue-labels: 'work-in-progress,blocked,help wanted,under investigation' 28 | -------------------------------------------------------------------------------- /.github/workflows/workflow_updater.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Version Updater 2 | 3 | # Controls when the action will run. 4 | on: 5 | workflow_dispatch: 6 | 7 | schedule: 8 | # Automatically run on every Sunday 9 | - cron: '0 0 1 * *' 10 | 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4.1.0 18 | with: 19 | # [Required] Access token with `workflow` scope. 20 | token: ${{ secrets.WORKFLOW_SECRET }} 21 | 22 | - name: GitHub Actions Version Updater 23 | uses: saadmk11/github-actions-version-updater@v0.8.1 24 | with: 25 | # [Required] Access token with `workflow` scope. 26 | token: ${{ secrets.WORKFLOW_SECRET }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/** 2 | .vscode/** 3 | **/__pycache__/** 4 | Config/** 5 | .trunk/logs 6 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | # trunk-ignore(hadolint/DL3007) 2 | FROM gitpod/workspace-full:latest 3 | 4 | USER gitpod 5 | 6 | RUN pyenv install 3.9.7 &&\ 7 | pyenv global 3.9.7 &&\ 8 | echo "export PIP_USER=no" >> /home/gitpod/.bashrc &&\ 9 | python -m pip install --no-cache-dir virtualenv==20.14.1 10 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | tasks: 4 | - name: Requirements 5 | init: | 6 | python -m virtualenv .venv 7 | . .venv/bin/activate 8 | pip3 install pre-commit 9 | pre-commit install 10 | pre-commit run --all-files 11 | pip3 install -r requirements_dev.txt 12 | ports: 13 | - port: 9123 14 | onOpen: notify 15 | github: 16 | prebuilds: 17 | master: true 18 | branches: false 19 | pullRequests: true 20 | pullRequestsFromForks: true 21 | addCheck: true 22 | addComment: false 23 | addBadge: false 24 | vscode: 25 | extensions: 26 | - esbenp.prettier-vscode 27 | - github.vscode-pull-request-github 28 | - eamodio.gitlens 29 | - ms-python.python 30 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- 1 | # Following source doesn't work in most setups 2 | ignored: 3 | - SC1090 4 | - SC1091 5 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile=black 3 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Autoformatter friendly markdownlint config (all formatting rules disabled) 2 | default: true 3 | blank_lines: false 4 | bullet: false 5 | html: false 6 | indentation: false 7 | line_length: false 8 | spaces: false 9 | url: false 10 | whitespace: false 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v3.3.1 4 | hooks: 5 | - id: pyupgrade 6 | args: [--py37-plus] 7 | - repo: https://github.com/psf/black 8 | rev: 23.1.0 9 | hooks: 10 | - id: black 11 | additional_dependencies: ['click==8.0.4'] 12 | args: 13 | - --safe 14 | - --quiet 15 | files: ^((custom_components)/.+)?[^/]+\.py$ 16 | - repo: https://github.com/codespell-project/codespell 17 | rev: v2.2.2 18 | hooks: 19 | - id: codespell 20 | args: 21 | - --ignore-words-list=threshHold,hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing 22 | - --skip="./.*,*.csv,*.json,*.md" 23 | - --quiet-level=2 24 | exclude_types: [csv, json] 25 | - repo: https://github.com/pycqa/flake8 26 | rev: 6.0.0 27 | hooks: 28 | - id: flake8 29 | additional_dependencies: 30 | - flake8-docstrings==1.5.0 31 | - pydocstyle==5.1.1 32 | files: ^(custom_components)/.+\.py$ 33 | - repo: https://github.com/PyCQA/isort 34 | rev: 5.12.0 35 | hooks: 36 | - id: isort 37 | - repo: https://github.com/adrienverge/yamllint.git 38 | rev: v1.29.0 39 | hooks: 40 | - id: yamllint 41 | -------------------------------------------------------------------------------- /.trunk/.gitignore: -------------------------------------------------------------------------------- 1 | *out 2 | -------------------------------------------------------------------------------- /.trunk/trunk.yaml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | cli: 3 | version: 0.15.0-beta 4 | lint: 5 | enabled: 6 | - actionlint@1.6.9 7 | - black@22.3.0 8 | - flake8@4.0.1 9 | - gitleaks@8.3.0 10 | - hadolint@2.8.0 11 | - isort@5.9.3 12 | - markdownlint@0.31.1 13 | - prettier@2.5.1 14 | - taplo@release-cli-0.6.0 15 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | ignore: | 2 | .github 3 | rules: 4 | braces: 5 | level: error 6 | min-spaces-inside: 0 7 | max-spaces-inside: 1 8 | min-spaces-inside-empty: -1 9 | max-spaces-inside-empty: -1 10 | brackets: 11 | level: error 12 | min-spaces-inside: 0 13 | max-spaces-inside: 0 14 | min-spaces-inside-empty: -1 15 | max-spaces-inside-empty: -1 16 | colons: 17 | level: error 18 | max-spaces-before: 0 19 | max-spaces-after: 1 20 | commas: 21 | level: error 22 | max-spaces-before: 0 23 | min-spaces-after: 1 24 | max-spaces-after: 1 25 | comments: 26 | level: error 27 | require-starting-space: true 28 | min-spaces-from-content: 2 29 | comments-indentation: 30 | level: error 31 | document-end: 32 | level: error 33 | present: false 34 | document-start: 35 | level: error 36 | present: false 37 | empty-lines: 38 | level: error 39 | max: 1 40 | max-start: 0 41 | max-end: 1 42 | hyphens: 43 | level: error 44 | max-spaces-after: 1 45 | indentation: 46 | level: error 47 | spaces: 2 48 | indent-sequences: true 49 | check-multi-line-strings: false 50 | key-duplicates: 51 | level: error 52 | line-length: disable 53 | new-line-at-end-of-file: 54 | level: error 55 | new-lines: 56 | level: error 57 | type: unix 58 | trailing-spaces: 59 | level: error 60 | truthy: 61 | level: warning 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # **Important message** 3 | > 4 | > This a fork of the existing archived project created by vlebourl. Please contribute here. 5 | 6 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) 7 | [![GitHub release](https://img.shields.io/github/v/release/vlebourl/custom_vesync.svg)](https://GitHub.com/vlebourl/custom_vesync/releases/) 8 | 9 | # VeSync custom component for Home Assistant 10 | 11 | Custom component for Home Assistant to interact with smart devices via the VeSync platform. 12 | This integration is heavily based on [VeSync_bpo](https://github.com/borpin/vesync-bpo) and relies on [pyvesync](https://github.com/webdjoe/pyvesync) under the hood. 13 | 14 | ## Installation 15 | 16 | You can install this integration via [HACS](#hacs) or [manually](#manual). 17 | This integration will override the core VeSync integration. 18 | 19 | ### HACS 20 | 21 | This integration can be installed by adding this repository to HACS __AS A CUSTOM REPOSITORY__, then searching for `Custom VeSync`, and choosing install. Reboot Home Assistant and configure the 'VeSync' integration via the integrations page or press the blue button below. 22 | 23 | [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=vesync) 24 | 25 | ### Manual 26 | 27 | Copy the `custom_components/vesync` to your `custom_components` folder. Reboot Home Assistant and configure the 'VeSync' integration via the integrations page or press the blue button below. 28 | 29 | [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=vesync) 30 | 31 | You can make sure the custom integration is in use by looking for the following icon in the Settings > Devices & Services page: 32 | ![image](https://user-images.githubusercontent.com/5701372/234820776-11a80f79-5b4d-4dbe-8b63-42579e4a5631.png) 33 | 34 | ## Logging 35 | 36 | ### Enable debug logging 37 | 38 | The [logger](https://www.home-assistant.io/integrations/logger/) integration lets you define the level of logging activities in Home Assistant. Turning on debug mode will show more information about unsupported devices in your logbook. 39 | 40 | ```yaml 41 | logger: 42 | default: error 43 | logs: 44 | custom_components.vesync: debug 45 | pyvesync: debug 46 | ``` 47 | 48 | ## TODO LIST 49 | ``` 50 | - [x] Air Fryer Properties (AirFryer158) 51 | - [ ] Air Fryer Methods 52 | - [ ] Create the Card 53 | ``` 54 | 55 | ### Contributing 56 | 57 | All contributions are very welcomed! 58 | Please make sure to install `pre-commit` and run the pre-commit hook before submitting a PR. 59 | 60 | ```sh 61 | pip install pre-commit 62 | pre-commit install 63 | pre-commit run --all-files 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /custom_components/vesync/__init__.py: -------------------------------------------------------------------------------- 1 | """VeSync integration.""" 2 | 3 | from datetime import timedelta 4 | import logging 5 | 6 | from pyvesync.vesync import VeSync 7 | 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform 10 | from homeassistant.core import HomeAssistant, ServiceCall 11 | from homeassistant.helpers import config_validation as cv 12 | from homeassistant.helpers.dispatcher import async_dispatcher_send 13 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 14 | 15 | from .common import async_process_devices 16 | from .const import ( 17 | DOMAIN, 18 | SERVICE_UPDATE_DEVS, 19 | VS_BINARY_SENSORS, 20 | VS_BUTTON, 21 | VS_DISCOVERY, 22 | VS_FANS, 23 | VS_HUMIDIFIERS, 24 | VS_LIGHTS, 25 | VS_MANAGER, 26 | VS_NUMBERS, 27 | VS_SENSORS, 28 | VS_SWITCHES, 29 | ) 30 | 31 | PLATFORMS = { 32 | Platform.SWITCH: VS_SWITCHES, 33 | Platform.FAN: VS_FANS, 34 | Platform.LIGHT: VS_LIGHTS, 35 | Platform.SENSOR: VS_SENSORS, 36 | Platform.HUMIDIFIER: VS_HUMIDIFIERS, 37 | Platform.NUMBER: VS_NUMBERS, 38 | Platform.BINARY_SENSOR: VS_BINARY_SENSORS, 39 | Platform.BUTTON: VS_BUTTON, 40 | } 41 | 42 | _LOGGER = logging.getLogger(__name__) 43 | 44 | CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) 45 | 46 | 47 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 48 | """Set up Vesync as config entry.""" 49 | username = config_entry.data[CONF_USERNAME] 50 | password = config_entry.data[CONF_PASSWORD] 51 | 52 | time_zone = str(hass.config.time_zone) 53 | 54 | manager = VeSync(username, password, time_zone) 55 | 56 | login = await hass.async_add_executor_job(manager.login) 57 | 58 | if not login: 59 | _LOGGER.error("Unable to login to the VeSync server") 60 | return False 61 | 62 | forward_setup = hass.config_entries.async_forward_entry_setup 63 | 64 | hass.data[DOMAIN] = {config_entry.entry_id: {}} 65 | hass.data[DOMAIN][config_entry.entry_id][VS_MANAGER] = manager 66 | 67 | # Create a DataUpdateCoordinator for the manager 68 | async def async_update_data(): 69 | """Fetch data from API endpoint.""" 70 | try: 71 | await hass.async_add_executor_job(manager.update) 72 | except Exception as err: 73 | raise UpdateFailed(f"Update failed: {err}") from err 74 | 75 | coordinator = DataUpdateCoordinator( 76 | hass, 77 | _LOGGER, 78 | name="vesync", 79 | update_method=async_update_data, 80 | update_interval=timedelta(seconds=30), 81 | ) 82 | 83 | # Fetch initial data so we have data when entities subscribe 84 | await coordinator.async_refresh() 85 | 86 | # Store the coordinator instance in hass.data 87 | hass.data[DOMAIN][config_entry.entry_id]["coordinator"] = coordinator 88 | 89 | device_dict = await async_process_devices(hass, manager) 90 | 91 | for p, vs_p in PLATFORMS.items(): 92 | hass.data[DOMAIN][config_entry.entry_id][vs_p] = [] 93 | if device_dict[vs_p]: 94 | hass.data[DOMAIN][config_entry.entry_id][vs_p].extend(device_dict[vs_p]) 95 | hass.async_create_task(forward_setup(config_entry, p)) 96 | 97 | async def async_new_device_discovery(service: ServiceCall) -> None: 98 | """Discover if new devices should be added.""" 99 | manager = hass.data[DOMAIN][config_entry.entry_id][VS_MANAGER] 100 | dev_dict = await async_process_devices(hass, manager) 101 | 102 | def _add_new_devices(platform: str) -> None: 103 | """Add new devices to hass.""" 104 | old_devices = hass.data[DOMAIN][config_entry.entry_id][PLATFORMS[platform]] 105 | if new_devices := list( 106 | set(dev_dict.get(VS_SWITCHES, [])).difference(old_devices) 107 | ): 108 | old_devices.extend(new_devices) 109 | if old_devices: 110 | async_dispatcher_send( 111 | hass, VS_DISCOVERY.format(PLATFORMS[platform]), new_devices 112 | ) 113 | else: 114 | hass.async_create_task(forward_setup(config_entry, platform)) 115 | 116 | for k in PLATFORMS: 117 | _add_new_devices(k) 118 | 119 | hass.services.async_register( 120 | DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery 121 | ) 122 | 123 | return True 124 | 125 | 126 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 127 | """Unload a config entry.""" 128 | unload_ok = await hass.config_entries.async_unload_platforms( 129 | entry, list(PLATFORMS.keys()) 130 | ) 131 | if unload_ok: 132 | hass.data[DOMAIN].pop(entry.entry_id) 133 | 134 | return unload_ok 135 | -------------------------------------------------------------------------------- /custom_components/vesync/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for power & energy sensors for VeSync outlets.""" 2 | 3 | import logging 4 | 5 | from homeassistant.components.binary_sensor import BinarySensorEntity 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant, callback 8 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 9 | from homeassistant.helpers.entity import EntityCategory 10 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 11 | 12 | from .common import VeSyncBaseEntity, has_feature 13 | from .const import BINARY_SENSOR_TYPES_AIRFRYER, DOMAIN, VS_BINARY_SENSORS, VS_DISCOVERY 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | 18 | async def async_setup_entry( 19 | hass: HomeAssistant, 20 | config_entry: ConfigEntry, 21 | async_add_entities: AddEntitiesCallback, 22 | ) -> None: 23 | """Set up binary sensors.""" 24 | 25 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 26 | 27 | @callback 28 | def discover(devices): 29 | """Add new devices to platform.""" 30 | _setup_entities(devices, async_add_entities, coordinator) 31 | 32 | config_entry.async_on_unload( 33 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_BINARY_SENSORS), discover) 34 | ) 35 | 36 | _setup_entities( 37 | hass.data[DOMAIN][config_entry.entry_id][VS_BINARY_SENSORS], 38 | async_add_entities, 39 | coordinator, 40 | ) 41 | 42 | 43 | @callback 44 | def _setup_entities(devices, async_add_entities, coordinator): 45 | """Check if device is online and add entity.""" 46 | entities = [] 47 | for dev in devices: 48 | if hasattr(dev, "fryer_status"): 49 | for stype in BINARY_SENSOR_TYPES_AIRFRYER.values(): 50 | entities.append( # noqa: PERF401 51 | VeSyncairfryerSensor( 52 | dev, 53 | coordinator, 54 | stype, 55 | ) 56 | ) 57 | if has_feature(dev, "details", "water_lacks"): 58 | entities.append(VeSyncOutOfWaterSensor(dev, coordinator)) 59 | if has_feature(dev, "details", "water_tank_lifted"): 60 | entities.append(VeSyncWaterTankLiftedSensor(dev, coordinator)) 61 | if has_feature(dev, "details", "filter_open_state"): 62 | entities.append(VeSyncFilterOpenStateSensor(dev, coordinator)) 63 | 64 | async_add_entities(entities, update_before_add=True) 65 | 66 | 67 | class VeSyncairfryerSensor(VeSyncBaseEntity, BinarySensorEntity): 68 | """Class representing a VeSyncairfryerSensor.""" 69 | 70 | def __init__(self, airfryer, coordinator, stype) -> None: 71 | """Initialize the VeSync humidifier device.""" 72 | super().__init__(airfryer, coordinator) 73 | self.airfryer = airfryer 74 | self.stype = stype 75 | 76 | @property 77 | def entity_category(self): 78 | """Return the diagnostic entity category.""" 79 | return EntityCategory.DIAGNOSTIC 80 | 81 | @property 82 | def unique_id(self): 83 | """Return unique ID for water tank lifted sensor on device.""" 84 | return f"{super().unique_id}-" + self.stype[0] 85 | 86 | @property 87 | def name(self): 88 | """Return sensor name.""" 89 | return self.stype[1] 90 | 91 | @property 92 | def is_on(self) -> bool: 93 | """Return a value indicating whether the Humidifier's water tank is lifted.""" 94 | return getattr(self.airfryer, self.stype[0], None) 95 | # return self.smarthumidifier.details["water_tank_lifted"] 96 | 97 | @property 98 | def icon(self): 99 | """Return the icon to use in the frontend, if any.""" 100 | return self.stype[2] 101 | 102 | 103 | class VeSyncBinarySensorEntity(VeSyncBaseEntity, BinarySensorEntity): 104 | """Representation of a binary sensor describing diagnostics of a VeSync humidifier.""" 105 | 106 | def __init__(self, humidifier, coordinator) -> None: 107 | """Initialize the VeSync humidifier device.""" 108 | super().__init__(humidifier, coordinator) 109 | self.smarthumidifier = humidifier 110 | 111 | @property 112 | def entity_category(self): 113 | """Return the diagnostic entity category.""" 114 | return EntityCategory.DIAGNOSTIC 115 | 116 | 117 | class VeSyncOutOfWaterSensor(VeSyncBinarySensorEntity): 118 | """Out of Water Sensor.""" 119 | 120 | @property 121 | def unique_id(self): 122 | """Return unique ID for out of water sensor on device.""" 123 | return f"{super().unique_id}-out_of_water" 124 | 125 | @property 126 | def name(self): 127 | """Return sensor name.""" 128 | return f"{super().name} out of water" 129 | 130 | @property 131 | def is_on(self) -> bool: 132 | """Return a value indicating whether the Humidifier is out of water.""" 133 | return self.smarthumidifier.details["water_lacks"] 134 | 135 | 136 | class VeSyncWaterTankLiftedSensor(VeSyncBinarySensorEntity): 137 | """Tank Lifted Sensor.""" 138 | 139 | @property 140 | def unique_id(self): 141 | """Return unique ID for water tank lifted sensor on device.""" 142 | return f"{super().unique_id}-water_tank_lifted" 143 | 144 | @property 145 | def name(self): 146 | """Return sensor name.""" 147 | return f"{super().name} water tank lifted" 148 | 149 | @property 150 | def is_on(self) -> bool: 151 | """Return a value indicating whether the Humidifier's water tank is lifted.""" 152 | return self.smarthumidifier.details["water_tank_lifted"] 153 | 154 | 155 | class VeSyncFilterOpenStateSensor(VeSyncBinarySensorEntity): 156 | """Filter Open Sensor.""" 157 | 158 | @property 159 | def unique_id(self): 160 | """Return unique ID for filter open state sensor on device.""" 161 | return f"{super().unique_id}-filter-open-state" 162 | 163 | @property 164 | def name(self): 165 | """Return sensor name.""" 166 | return f"{super().name} filter open state" 167 | 168 | @property 169 | def is_on(self) -> bool: 170 | """Return a value indicating whether the Humidifier's filter is open.""" 171 | return self.smarthumidifier.details["filter_open_state"] 172 | -------------------------------------------------------------------------------- /custom_components/vesync/button.py: -------------------------------------------------------------------------------- 1 | """Support for VeSync button.""" 2 | 3 | import logging 4 | 5 | from homeassistant.components.button import ButtonEntity 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant, callback 8 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | 11 | from .common import VeSyncBaseEntity 12 | from .const import DOMAIN, VS_BUTTON, VS_DISCOVERY 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | SENSOR_TYPES_CS158 = { 18 | # unique_id,name # icon, 19 | "end": [ 20 | "end", 21 | "End cooking or preheating ", 22 | "mdi:stop", 23 | ], 24 | } 25 | 26 | 27 | async def async_setup_entry( 28 | hass: HomeAssistant, 29 | config_entry: ConfigEntry, 30 | async_add_entities: AddEntitiesCallback, 31 | ) -> None: 32 | """Set up switches.""" 33 | 34 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 35 | 36 | @callback 37 | def discover(devices): 38 | """Add new devices to platform.""" 39 | _setup_entities(devices, async_add_entities, coordinator) 40 | 41 | config_entry.async_on_unload( 42 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_BUTTON), discover) 43 | ) 44 | 45 | _setup_entities( 46 | hass.data[DOMAIN][config_entry.entry_id][VS_BUTTON], 47 | async_add_entities, 48 | coordinator, 49 | ) 50 | 51 | 52 | @callback 53 | def _setup_entities(devices, async_add_entities, coordinator): 54 | """Check if device is online and add entity.""" 55 | entities = [] 56 | for dev in devices: 57 | if hasattr(dev, "cook_set_temp"): 58 | for stype in SENSOR_TYPES_CS158.values(): 59 | entities.append( # noqa: PERF401 60 | VeSyncairfryerButton( 61 | dev, 62 | coordinator, 63 | stype, 64 | ) 65 | ) 66 | 67 | async_add_entities(entities, update_before_add=True) 68 | 69 | 70 | class VeSyncairfryerButton(VeSyncBaseEntity, ButtonEntity): 71 | """Base class for VeSync switch Device Representations.""" 72 | 73 | def __init__(self, airfryer, coordinator, stype) -> None: 74 | """Initialize the VeSync humidifier device.""" 75 | super().__init__(airfryer, coordinator) 76 | self.airfryer = airfryer 77 | self.stype = stype 78 | 79 | @property 80 | def unique_id(self): 81 | """Return unique ID for water tank lifted sensor on device.""" 82 | return f"{super().unique_id}-" + self.stype[0] 83 | 84 | @property 85 | def name(self): 86 | """Return sensor name.""" 87 | return self.stype[1] 88 | 89 | @property 90 | def icon(self): 91 | """Return the icon to use in the frontend, if any.""" 92 | return self.stype[2] 93 | 94 | def press(self) -> None: 95 | """Return True if device is on.""" 96 | self.airfryer.end() 97 | -------------------------------------------------------------------------------- /custom_components/vesync/common.py: -------------------------------------------------------------------------------- 1 | """Common utilities for VeSync Component.""" 2 | import logging 3 | 4 | from pyvesync.vesyncfan import model_features as fan_model_features 5 | from pyvesync.vesynckitchen import model_features as kitchen_model_features 6 | 7 | from homeassistant.components.diagnostics import async_redact_data 8 | from homeassistant.helpers.entity import Entity, ToggleEntity 9 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 10 | 11 | from .const import ( 12 | DOMAIN, 13 | VS_AIRFRYER_TYPES, 14 | VS_BINARY_SENSORS, 15 | VS_BUTTON, 16 | VS_FAN_TYPES, 17 | VS_FANS, 18 | VS_HUMIDIFIERS, 19 | VS_HUMIDIFIERS_TYPES, 20 | VS_LIGHTS, 21 | VS_NUMBERS, 22 | VS_SENSORS, 23 | VS_SWITCHES, 24 | ) 25 | 26 | _LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | def has_feature(device, dictionary, attribute): 30 | """Return the detail of the attribute.""" 31 | return getattr(device, dictionary, {}).get(attribute, None) is not None 32 | 33 | 34 | async def async_process_devices(hass, manager): 35 | """Assign devices to proper component.""" 36 | devices = { 37 | VS_SWITCHES: [], 38 | VS_FANS: [], 39 | VS_LIGHTS: [], 40 | VS_SENSORS: [], 41 | VS_HUMIDIFIERS: [], 42 | VS_NUMBERS: [], 43 | VS_BINARY_SENSORS: [], 44 | VS_BUTTON: [], 45 | } 46 | 47 | redacted = async_redact_data( 48 | {k: [d.__dict__ for d in v] for k, v in manager._dev_list.items()}, 49 | ["cid", "uuid", "mac_id"], 50 | ) 51 | 52 | _LOGGER.warning( 53 | "Found the following devices: %s", 54 | redacted, 55 | ) 56 | 57 | if ( 58 | manager.bulbs is None 59 | and manager.fans is None 60 | and manager.kitchen is None 61 | and manager.outlets is None 62 | and manager.switches is None 63 | ): 64 | _LOGGER.error("Could not find any device to add in %s", redacted) 65 | 66 | if manager.fans: 67 | for fan in manager.fans: 68 | # VeSync classifies humidifiers as fans 69 | if fan_model_features(fan.device_type)["module"] in VS_HUMIDIFIERS_TYPES: 70 | devices[VS_HUMIDIFIERS].append(fan) 71 | elif fan_model_features(fan.device_type)["module"] in VS_FAN_TYPES: 72 | devices[VS_FANS].append(fan) 73 | else: 74 | _LOGGER.warning( 75 | "Unknown fan type %s %s (enable debug for more info)", 76 | fan.device_name, 77 | fan.device_type, 78 | ) 79 | continue 80 | devices[VS_NUMBERS].append(fan) 81 | devices[VS_SWITCHES].append(fan) 82 | devices[VS_SENSORS].append(fan) 83 | devices[VS_BINARY_SENSORS].append(fan) 84 | devices[VS_LIGHTS].append(fan) 85 | 86 | if manager.bulbs: 87 | devices[VS_LIGHTS].extend(manager.bulbs) 88 | 89 | if manager.outlets: 90 | devices[VS_SWITCHES].extend(manager.outlets) 91 | # Expose outlets' power & energy usage as separate sensors 92 | devices[VS_SENSORS].extend(manager.outlets) 93 | 94 | if manager.switches: 95 | for switch in manager.switches: 96 | if not switch.is_dimmable(): 97 | devices[VS_SWITCHES].append(switch) 98 | else: 99 | devices[VS_LIGHTS].append(switch) 100 | 101 | if manager.kitchen: 102 | for airfryer in manager.kitchen: 103 | if ( 104 | kitchen_model_features(airfryer.device_type)["module"] 105 | in VS_AIRFRYER_TYPES 106 | ): 107 | _LOGGER.warning( 108 | "Found air fryer %s, support in progress.\n", airfryer.device_name 109 | ) 110 | devices[VS_SENSORS].append(airfryer) 111 | devices[VS_BINARY_SENSORS].append(airfryer) 112 | devices[VS_SWITCHES].append(airfryer) 113 | devices[VS_BUTTON].append(airfryer) 114 | else: 115 | _LOGGER.warning( 116 | "Unknown device type %s %s (enable debug for more info)", 117 | airfryer.device_name, 118 | airfryer.device_type, 119 | ) 120 | 121 | return devices 122 | 123 | 124 | class VeSyncBaseEntity(CoordinatorEntity, Entity): 125 | """Base class for VeSync Entity Representations.""" 126 | 127 | def __init__(self, device, coordinator) -> None: 128 | """Initialize the VeSync device.""" 129 | self.device = device 130 | super().__init__(coordinator, context=device) 131 | 132 | @property 133 | def base_unique_id(self): 134 | """Return the ID of this device.""" 135 | if isinstance(self.device.sub_device_no, int): 136 | return f"{self.device.cid}{str(self.device.sub_device_no)}" 137 | return self.device.cid 138 | 139 | @property 140 | def unique_id(self): 141 | """Return the ID of this device.""" 142 | # The unique_id property may be overridden in subclasses, such as in sensors. Maintaining base_unique_id allows 143 | # us to group related entities under a single device. 144 | return self.base_unique_id 145 | 146 | @property 147 | def base_name(self): 148 | """Return the name of the device.""" 149 | return self.device.device_name 150 | 151 | @property 152 | def name(self): 153 | """Return the name of the entity (may be overridden).""" 154 | return self.base_name 155 | 156 | @property 157 | def available(self) -> bool: 158 | """Return True if device is available.""" 159 | return self.device.connection_status == "online" 160 | 161 | @property 162 | def device_info(self): 163 | """Return device information.""" 164 | return { 165 | "identifiers": {(DOMAIN, self.base_unique_id)}, 166 | "name": self.base_name, 167 | "model": self.device.device_type, 168 | "manufacturer": "VeSync", 169 | "sw_version": self.device.current_firm_version, 170 | } 171 | 172 | async def async_added_to_hass(self): 173 | """When entity is added to hass.""" 174 | self.async_on_remove( 175 | self.coordinator.async_add_listener(self.async_write_ha_state) 176 | ) 177 | 178 | 179 | class VeSyncDevice(VeSyncBaseEntity, ToggleEntity): 180 | """Base class for VeSync Device Representations.""" 181 | 182 | def __init__(self, device, coordinator) -> None: 183 | """Initialize the VeSync device.""" 184 | super().__init__(device, coordinator) 185 | 186 | @property 187 | def is_on(self): 188 | """Return True if device is on.""" 189 | return self.device.device_status == "on" 190 | 191 | def turn_off(self, **kwargs): 192 | """Turn the device off.""" 193 | self.device.turn_off() 194 | -------------------------------------------------------------------------------- /custom_components/vesync/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow utilities.""" 2 | from collections import OrderedDict 3 | import logging 4 | 5 | from pyvesync.vesync import VeSync 6 | import voluptuous as vol 7 | 8 | from homeassistant import config_entries 9 | from homeassistant.components import dhcp 10 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 11 | from homeassistant.core import callback 12 | from homeassistant.data_entry_flow import FlowResult 13 | 14 | from .const import DOMAIN 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 20 | """Handle a config flow.""" 21 | 22 | VERSION = 1 23 | 24 | def __init__(self) -> None: 25 | """Instantiate config flow.""" 26 | self._username = None 27 | self._password = None 28 | self.data_schema = OrderedDict() 29 | self.data_schema[vol.Required(CONF_USERNAME)] = str 30 | self.data_schema[vol.Required(CONF_PASSWORD)] = str 31 | 32 | @callback 33 | def _show_form(self, errors=None): 34 | """Show form to the user.""" 35 | return self.async_show_form( 36 | step_id="user", 37 | data_schema=vol.Schema(self.data_schema), 38 | errors=errors or {}, 39 | ) 40 | 41 | async def async_step_user(self, user_input=None): 42 | """Handle a flow start.""" 43 | if self._async_current_entries(): 44 | return self.async_abort(reason="single_instance_allowed") 45 | 46 | if not user_input: 47 | return self._show_form() 48 | 49 | self._username = user_input[CONF_USERNAME] 50 | self._password = user_input[CONF_PASSWORD] 51 | 52 | manager = VeSync(self._username, self._password) 53 | login = await self.hass.async_add_executor_job(manager.login) 54 | await self.async_set_unique_id(f"{self._username}-{manager.account_id}") 55 | self._abort_if_unique_id_configured() 56 | 57 | return ( 58 | self.async_create_entry( 59 | title=self._username, 60 | data={ 61 | CONF_USERNAME: self._username, 62 | CONF_PASSWORD: self._password, 63 | }, 64 | ) 65 | if login 66 | else self._show_form(errors={"base": "invalid_auth"}) 67 | ) 68 | 69 | async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: 70 | """Handle DHCP discovery.""" 71 | hostname = discovery_info.hostname 72 | 73 | _LOGGER.debug("DHCP discovery detected device %s", hostname) 74 | self.context["title_placeholders"] = {"gateway_id": hostname} 75 | return await self.async_step_user() 76 | -------------------------------------------------------------------------------- /custom_components/vesync/const.py: -------------------------------------------------------------------------------- 1 | """Constants for VeSync Component.""" 2 | 3 | from homeassistant.components.sensor import SensorDeviceClass 4 | from homeassistant.const import UnitOfTemperature, UnitOfTime 5 | 6 | DOMAIN = "vesync" 7 | VS_DISCOVERY = "vesync_discovery_{}" 8 | SERVICE_UPDATE_DEVS = "update_devices" 9 | 10 | VS_BUTTON = "button" 11 | VS_SWITCHES = "switches" 12 | VS_FAN = "fan" 13 | VS_FANS = "fans" 14 | VS_LIGHTS = "lights" 15 | VS_SENSORS = "sensors" 16 | VS_HUMIDIFIERS = "humidifiers" 17 | VS_NUMBERS = "numbers" 18 | VS_BINARY_SENSORS = "binary_sensors" 19 | VS_MANAGER = "manager" 20 | 21 | VS_LEVELS = "levels" 22 | VS_MODES = "modes" 23 | 24 | VS_MODE_AUTO = "auto" 25 | VS_MODE_HUMIDITY = "humidity" 26 | VS_MODE_MANUAL = "manual" 27 | VS_MODE_SLEEP = "sleep" 28 | VS_MODE_TURBO = "turbo" 29 | 30 | VS_TO_HA_ATTRIBUTES = {"humidity": "current_humidity"} 31 | 32 | VS_FAN_TYPES = ["VeSyncAirBypass", "VeSyncAir131", "VeSyncAirBaseV2"] 33 | VS_HUMIDIFIERS_TYPES = ["VeSyncHumid200300S", "VeSyncHumid200S", "VeSyncHumid1000S"] 34 | VS_AIRFRYER_TYPES = ["VeSyncAirFryer158"] 35 | 36 | 37 | DEV_TYPE_TO_HA = { 38 | "ESL100": "bulb-dimmable", 39 | "ESL100CW": "bulb-tunable-white", 40 | "ESO15-TB": "outlet", 41 | "ESW03-USA": "outlet", 42 | "ESW01-EU": "outlet", 43 | "ESW15-USA": "outlet", 44 | "wifi-switch-1.3": "outlet", 45 | "ESWL01": "switch", 46 | "ESWL03": "switch", 47 | "ESD16": "walldimmer", 48 | "ESWD16": "walldimmer", 49 | } 50 | 51 | 52 | BINARY_SENSOR_TYPES_AIRFRYER = { 53 | # unique_id,name # icon, #attribute read, 54 | "is_heating": [ 55 | "is_heating", 56 | "preheating", 57 | "mdi:pot-steam-outline", 58 | ], 59 | "is_cooking": [ 60 | "is_cooking", 61 | "cooking", 62 | "mdi:rice", 63 | ], 64 | "is_running": [ 65 | "is_running", 66 | "running", 67 | "mdi:pause", 68 | ], 69 | } 70 | 71 | 72 | SENSOR_TYPES_AIRFRYER = { 73 | # unique_id ,#name ,# unit of measurement,# icon, # device class, #attribute read, 74 | "current_temp": [ 75 | "current_temperature", 76 | "Current temperature", 77 | UnitOfTemperature.CELSIUS, 78 | None, 79 | SensorDeviceClass.TEMPERATURE, 80 | "current_temp", 81 | ], 82 | "cook_set_temp": [ 83 | "set_temperature", 84 | "Set temperature", 85 | UnitOfTemperature.CELSIUS, 86 | None, 87 | SensorDeviceClass.TEMPERATURE, 88 | "cook_set_temp", 89 | ], 90 | "cook_last_time": [ 91 | "cook_last_time", 92 | "Cook Remaining", 93 | UnitOfTime.MINUTES, 94 | "mdi:timer", 95 | UnitOfTime.MINUTES, 96 | "cook_last_time", 97 | ], 98 | "preheat_last_time": [ 99 | "preheat_last_time", 100 | "Preheat Remaining", 101 | UnitOfTime.MINUTES, 102 | "mdi:timer", 103 | UnitOfTime.MINUTES, 104 | "preheat_last_time", 105 | ], 106 | "cook_status": [ 107 | "cook_status", 108 | "Cook Status", 109 | None, 110 | "mdi:rotate-3d-variant", 111 | None, 112 | "cook_status", 113 | ], 114 | # "remaining_time": [ 115 | # "remaining_time", 116 | # "running:", 117 | # UnitOfTime.MINUTES, 118 | # "mdi:timer", 119 | # UnitOfTime.MINUTES, 120 | # "remaining_time", 121 | # ], 122 | } 123 | -------------------------------------------------------------------------------- /custom_components/vesync/device_action.py: -------------------------------------------------------------------------------- 1 | """Provides device actions for Humidifier.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | import voluptuous as vol 7 | 8 | from homeassistant.components.device_automation import toggle_entity 9 | from homeassistant.const import ( 10 | ATTR_ENTITY_ID, 11 | ATTR_MODE, 12 | CONF_DEVICE_ID, 13 | CONF_DOMAIN, 14 | CONF_ENTITY_ID, 15 | CONF_TYPE, 16 | ) 17 | from homeassistant.core import Context, HomeAssistant 18 | from homeassistant.exceptions import HomeAssistantError 19 | from homeassistant.helpers import entity_registry as er 20 | import homeassistant.helpers.config_validation as cv 21 | from homeassistant.helpers.entity import get_capability 22 | from homeassistant.helpers.typing import ConfigType, TemplateVarsType 23 | 24 | from .const import DOMAIN 25 | 26 | _LOGGER = logging.getLogger(__name__) 27 | 28 | # mypy: disallow-any-generics 29 | 30 | SET_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( 31 | { 32 | vol.Required(CONF_TYPE): "set_mode", 33 | vol.Required(CONF_ENTITY_ID): cv.entity_domain("fan"), 34 | vol.Required(ATTR_MODE): cv.string, 35 | } 36 | ) 37 | 38 | ACTION_SCHEMA = vol.Any(SET_MODE_SCHEMA) 39 | 40 | 41 | async def async_get_actions( 42 | hass: HomeAssistant, device_id: str 43 | ) -> list[dict[str, str]]: 44 | """List device actions for Humidifier devices.""" 45 | registry = er.async_get(hass) 46 | actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN) 47 | 48 | # Get all the integrations entities for this device 49 | for entry in er.async_entries_for_device(registry, device_id): 50 | if entry.domain != "fan": 51 | continue 52 | 53 | base_action = { 54 | CONF_DEVICE_ID: device_id, 55 | CONF_DOMAIN: DOMAIN, 56 | CONF_ENTITY_ID: entry.entity_id, 57 | } 58 | 59 | actions.append({**base_action, CONF_TYPE: "set_mode"}) 60 | 61 | return actions 62 | 63 | 64 | async def async_call_action_from_config( 65 | hass: HomeAssistant, 66 | config: ConfigType, 67 | variables: TemplateVarsType, 68 | context: Context | None, 69 | ) -> None: 70 | """Execute a device action.""" 71 | service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} 72 | 73 | if config[CONF_TYPE] != "set_mode": 74 | return await toggle_entity.async_call_action_from_config( 75 | hass, config, variables, context, DOMAIN 76 | ) 77 | 78 | service = "set_preset_mode" 79 | service_data["preset_mode"] = config[ATTR_MODE] 80 | await hass.services.async_call( 81 | "fan", service, service_data, blocking=True, context=context 82 | ) 83 | 84 | 85 | async def async_get_action_capabilities( 86 | hass: HomeAssistant, config: ConfigType 87 | ) -> dict[str, vol.Schema]: 88 | """List action capabilities.""" 89 | action_type = config[CONF_TYPE] 90 | 91 | if action_type != "set_mode": 92 | return {} 93 | 94 | try: 95 | available_modes = ( 96 | get_capability(hass, config[ATTR_ENTITY_ID], "preset_modes") or [] 97 | ) 98 | except HomeAssistantError: 99 | available_modes = [] 100 | fields = {vol.Required(ATTR_MODE): vol.In(available_modes)} 101 | return {"extra_fields": vol.Schema(fields)} 102 | -------------------------------------------------------------------------------- /custom_components/vesync/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Provides diagnostics for VeSync.""" 2 | from __future__ import annotations 3 | 4 | from typing import Any 5 | 6 | from homeassistant.components.diagnostics import async_redact_data 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.core import HomeAssistant 9 | 10 | # from .common import is_humidifier 11 | # from .const import DOMAIN 12 | 13 | TO_REDACT = {"cid", "uuid", "mac_id"} 14 | 15 | 16 | async def async_get_config_entry_diagnostics( 17 | hass: HomeAssistant, entry: ConfigEntry 18 | ) -> dict[str, Any]: 19 | """Return diagnostics for a config entry.""" 20 | # data = hass.data[DOMAIN][entry.entry_id] 21 | devices = {} 22 | 23 | # for type in ["fans", "outlets", "switches", "bulbs"]: 24 | # for d in data["manager"]._dev_list[type]: 25 | # t = "humidifier" if is_humidifier(d.device_type) else type 26 | # devices = { 27 | # **devices, 28 | # **{t: [{k: v for k, v in d.__dict__.items() if k != "manager"}]}, 29 | # } 30 | return async_redact_data(devices, TO_REDACT) 31 | -------------------------------------------------------------------------------- /custom_components/vesync/fan.py: -------------------------------------------------------------------------------- 1 | """Support for VeSync fans.""" 2 | 3 | import math 4 | 5 | from homeassistant.components.fan import FanEntity, FanEntityFeature 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant, callback 8 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | from homeassistant.util.percentage import ( 11 | int_states_in_range, 12 | percentage_to_ranged_value, 13 | ranged_value_to_percentage, 14 | ) 15 | 16 | from .common import VeSyncDevice, has_feature 17 | from .const import ( 18 | DOMAIN, 19 | VS_DISCOVERY, 20 | VS_FANS, 21 | VS_LEVELS, 22 | VS_MODE_AUTO, 23 | VS_MODE_MANUAL, 24 | VS_MODE_SLEEP, 25 | VS_MODE_TURBO, 26 | VS_MODES, 27 | VS_TO_HA_ATTRIBUTES, 28 | ) 29 | 30 | 31 | async def async_setup_entry( 32 | hass: HomeAssistant, 33 | config_entry: ConfigEntry, 34 | async_add_entities: AddEntitiesCallback, 35 | ) -> None: 36 | """Set up the VeSync fan platform.""" 37 | 38 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 39 | 40 | @callback 41 | def discover(devices): 42 | """Add new devices to platform.""" 43 | _setup_entities(devices, async_add_entities, coordinator) 44 | 45 | config_entry.async_on_unload( 46 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_FANS), discover) 47 | ) 48 | 49 | _setup_entities( 50 | hass.data[DOMAIN][config_entry.entry_id][VS_FANS], 51 | async_add_entities, 52 | coordinator, 53 | ) 54 | 55 | 56 | @callback 57 | def _setup_entities(devices, async_add_entities, coordinator): 58 | """Check if device is online and add entity.""" 59 | async_add_entities( 60 | [VeSyncFanHA(dev, coordinator) for dev in devices], update_before_add=True 61 | ) 62 | 63 | 64 | class VeSyncFanHA(VeSyncDevice, FanEntity): 65 | """Representation of a VeSync fan.""" 66 | 67 | def __init__(self, fan, coordinator) -> None: 68 | """Initialize the VeSync fan device.""" 69 | super().__init__(fan, coordinator) 70 | self.smartfan = fan 71 | self._speed_range = (1, 1) 72 | self._attr_preset_modes = [VS_MODE_MANUAL, VS_MODE_AUTO, VS_MODE_SLEEP] 73 | if has_feature(self.smartfan, "_config_dict", VS_LEVELS): 74 | self._speed_range = (1, max(self.smartfan._config_dict[VS_LEVELS])) 75 | if has_feature(self.smartfan, "_config_dict", VS_MODES): 76 | self._attr_preset_modes = [ 77 | VS_MODE_MANUAL, 78 | *[ 79 | mode 80 | for mode in [VS_MODE_AUTO, VS_MODE_SLEEP, VS_MODE_TURBO] 81 | if mode in self.smartfan._config_dict[VS_MODES] 82 | ], 83 | ] 84 | if self.smartfan.device_type == "LV-PUR131S": 85 | self._speed_range = (1, 3) 86 | 87 | @property 88 | def supported_features(self): 89 | """Flag supported features.""" 90 | return ( 91 | FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE 92 | if self.speed_count > 1 93 | else FanEntityFeature.SET_SPEED 94 | ) 95 | 96 | @property 97 | def percentage(self): 98 | """Return the current speed.""" 99 | if ( 100 | self.smartfan.mode == VS_MODE_MANUAL 101 | and (current_level := self.smartfan.fan_level) is not None 102 | ): 103 | return ranged_value_to_percentage(self._speed_range, current_level) 104 | return None 105 | 106 | @property 107 | def speed_count(self) -> int: 108 | """Return the number of speeds the fan supports.""" 109 | return int_states_in_range(self._speed_range) 110 | 111 | @property 112 | def preset_mode(self): 113 | """Get the current preset mode.""" 114 | return self.smartfan.mode 115 | 116 | @property 117 | def unique_info(self): 118 | """Return the ID of this fan.""" 119 | return self.smartfan.uuid 120 | 121 | @property 122 | def extra_state_attributes(self): 123 | """Return the state attributes of the fan.""" 124 | attr = {} 125 | for k, v in self.smartfan.details.items(): 126 | if k in VS_TO_HA_ATTRIBUTES: 127 | attr[VS_TO_HA_ATTRIBUTES[k]] = v 128 | elif k in self.state_attributes: 129 | attr[f"vs_{k}"] = v 130 | else: 131 | attr[k] = v 132 | return attr 133 | 134 | def set_percentage(self, percentage): 135 | """Set the speed of the device.""" 136 | if percentage == 0: 137 | self.smartfan.turn_off() 138 | return 139 | 140 | if not self.smartfan.is_on: 141 | self.smartfan.turn_on() 142 | 143 | self.smartfan.manual_mode() 144 | self.smartfan.change_fan_speed( 145 | math.ceil(percentage_to_ranged_value(self._speed_range, percentage)) 146 | ) 147 | self.schedule_update_ha_state() 148 | 149 | def set_preset_mode(self, preset_mode): 150 | """Set the preset mode of device.""" 151 | if preset_mode not in self.preset_modes: 152 | raise ValueError( 153 | "{preset_mode} is not one of the valid preset modes: {self.preset_modes}" 154 | ) 155 | 156 | if not self.smartfan.is_on: 157 | self.smartfan.turn_on() 158 | 159 | if preset_mode == VS_MODE_AUTO: 160 | self.smartfan.auto_mode() 161 | elif preset_mode == VS_MODE_SLEEP: 162 | self.smartfan.sleep_mode() 163 | elif preset_mode == VS_MODE_MANUAL: 164 | self.smartfan.manual_mode() 165 | elif preset_mode == VS_MODE_TURBO: 166 | self.smartfan.turbo_mode() 167 | 168 | self.schedule_update_ha_state() 169 | 170 | def turn_on( 171 | self, 172 | # speed: str | None = None, 173 | percentage: int | None = None, 174 | preset_mode: str | None = None, 175 | **kwargs, 176 | ) -> None: 177 | """Turn the device on.""" 178 | if preset_mode: 179 | self.set_preset_mode(preset_mode) 180 | return 181 | if percentage is None: 182 | percentage = 50 183 | self.set_percentage(percentage) 184 | -------------------------------------------------------------------------------- /custom_components/vesync/humidifier.py: -------------------------------------------------------------------------------- 1 | """Support for VeSync humidifiers.""" 2 | from __future__ import annotations 3 | 4 | from collections.abc import Mapping 5 | import logging 6 | from typing import Any 7 | 8 | from pyvesync.vesyncfan import VeSyncHumid200300S 9 | 10 | from homeassistant.components.humidifier import HumidifierEntity 11 | from homeassistant.components.humidifier.const import ( 12 | MODE_AUTO, 13 | MODE_NORMAL, 14 | MODE_SLEEP, 15 | HumidifierEntityFeature, 16 | ) 17 | from homeassistant.config_entries import ConfigEntry 18 | from homeassistant.core import HomeAssistant, callback 19 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 20 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 21 | 22 | from .common import VeSyncDevice 23 | from .const import ( 24 | DOMAIN, 25 | VS_DISCOVERY, 26 | VS_HUMIDIFIERS, 27 | VS_MODE_AUTO, 28 | VS_MODE_HUMIDITY, 29 | VS_MODE_MANUAL, 30 | VS_MODE_SLEEP, 31 | VS_TO_HA_ATTRIBUTES, 32 | ) 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | 37 | MAX_HUMIDITY = 80 38 | MIN_HUMIDITY = 30 39 | 40 | 41 | VS_TO_HA_MODE_MAP = { 42 | VS_MODE_AUTO: MODE_AUTO, 43 | VS_MODE_HUMIDITY: MODE_AUTO, 44 | VS_MODE_MANUAL: MODE_NORMAL, 45 | VS_MODE_SLEEP: MODE_SLEEP, 46 | } 47 | 48 | HA_TO_VS_MODE_MAP = {v: k for k, v in VS_TO_HA_MODE_MAP.items()} 49 | 50 | 51 | async def async_setup_entry( 52 | hass: HomeAssistant, 53 | config_entry: ConfigEntry, 54 | async_add_entities: AddEntitiesCallback, 55 | ) -> None: 56 | """Set up the VeSync humidifier platform.""" 57 | 58 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 59 | 60 | @callback 61 | def discover(devices): 62 | """Add new devices to platform.""" 63 | _setup_entities(devices, async_add_entities, coordinator) 64 | 65 | config_entry.async_on_unload( 66 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_HUMIDIFIERS), discover) 67 | ) 68 | 69 | _setup_entities( 70 | hass.data[DOMAIN][config_entry.entry_id][VS_HUMIDIFIERS], 71 | async_add_entities, 72 | coordinator, 73 | ) 74 | 75 | 76 | @callback 77 | def _setup_entities(devices, async_add_entities, coordinator): 78 | """Check if device is online and add entity.""" 79 | async_add_entities( 80 | [VeSyncHumidifierHA(dev, coordinator) for dev in devices], 81 | update_before_add=True, 82 | ) 83 | 84 | 85 | def _get_ha_mode(vs_mode: str) -> str | None: 86 | ha_mode = VS_TO_HA_MODE_MAP.get(vs_mode) 87 | if ha_mode is None: 88 | _LOGGER.warning("Unknown mode '%s'", vs_mode) 89 | return ha_mode 90 | 91 | 92 | def _get_vs_mode(ha_mode: str) -> str | None: 93 | vs_mode = HA_TO_VS_MODE_MAP.get(ha_mode) 94 | if vs_mode is None: 95 | _LOGGER.warning("Unknown mode '%s'", ha_mode) 96 | return vs_mode 97 | 98 | 99 | class VeSyncHumidifierHA(VeSyncDevice, HumidifierEntity): 100 | """Representation of a VeSync humidifier.""" 101 | 102 | _attr_max_humidity = MAX_HUMIDITY 103 | _attr_min_humidity = MIN_HUMIDITY 104 | 105 | def __init__(self, humidifier: VeSyncHumid200300S, coordinator) -> None: 106 | """Initialize the VeSync humidifier device.""" 107 | super().__init__(humidifier, coordinator) 108 | self.smarthumidifier = humidifier 109 | 110 | @property 111 | def available_modes(self) -> list[str]: 112 | """Return the available mist modes.""" 113 | modes = [] 114 | for vs_mode in self.smarthumidifier.mist_modes: 115 | ha_mode = _get_ha_mode(vs_mode) 116 | 117 | if ha_mode is None: 118 | continue 119 | 120 | modes.append(ha_mode) 121 | 122 | return modes 123 | 124 | @property 125 | def supported_features(self): 126 | """Flag supported features.""" 127 | return HumidifierEntityFeature.MODES 128 | 129 | @property 130 | def target_humidity(self) -> int: 131 | """Return the humidity we try to reach.""" 132 | return self.smarthumidifier.config["auto_target_humidity"] 133 | 134 | @property 135 | def mode(self) -> str | None: 136 | """Get the current preset mode.""" 137 | return _get_ha_mode(self.smarthumidifier.details["mode"]) 138 | 139 | @property 140 | def is_on(self) -> bool: 141 | """Return True if humidifier is on.""" 142 | return self.smarthumidifier.enabled # device_status is always on 143 | 144 | @property 145 | def unique_info(self) -> str: 146 | """Return the ID of this humidifier.""" 147 | return self.smarthumidifier.uuid 148 | 149 | @property 150 | def extra_state_attributes(self) -> Mapping[str, Any]: 151 | """Return the state attributes of the humidifier.""" 152 | 153 | attr = {} 154 | for k, v in self.smarthumidifier.details.items(): 155 | if k in VS_TO_HA_ATTRIBUTES: 156 | attr[VS_TO_HA_ATTRIBUTES[k]] = v 157 | elif k in self.state_attributes: 158 | attr[f"vs_{k}"] = v 159 | else: 160 | attr[k] = v 161 | return attr 162 | 163 | def set_humidity(self, humidity: int) -> None: 164 | """Set the target humidity of the device.""" 165 | if humidity not in range(self.min_humidity, self.max_humidity + 1): 166 | raise ValueError( 167 | "{humidity} is not between {self.min_humidity} and {self.max_humidity} (inclusive)" 168 | ) 169 | if self.smarthumidifier.set_humidity(humidity): 170 | self.schedule_update_ha_state() 171 | else: 172 | raise ValueError("An error occurred while setting humidity.") 173 | 174 | def set_mode(self, mode: str) -> None: 175 | """Set the mode of the device.""" 176 | if mode not in self.available_modes: 177 | raise ValueError( 178 | "{mode} is not one of the valid available modes: {self.available_modes}" 179 | ) 180 | if self.smarthumidifier.set_humidity_mode(_get_vs_mode(mode)): 181 | self.schedule_update_ha_state() 182 | else: 183 | raise ValueError("An error occurred while setting mode.") 184 | 185 | def turn_on( 186 | self, 187 | **kwargs, 188 | ) -> None: 189 | """Turn the device on.""" 190 | success = self.smarthumidifier.turn_on() 191 | if not success: 192 | raise ValueError("An error occurred while turning on.") 193 | 194 | def turn_off(self, **kwargs) -> None: 195 | """Turn the device off.""" 196 | success = self.smarthumidifier.turn_off() 197 | if not success: 198 | raise ValueError("An error occurred while turning off.") 199 | -------------------------------------------------------------------------------- /custom_components/vesync/light.py: -------------------------------------------------------------------------------- 1 | """Support for VeSync bulbs and wall dimmers.""" 2 | import logging 3 | 4 | from homeassistant.components.light import ( 5 | ATTR_BRIGHTNESS, 6 | ATTR_COLOR_TEMP, 7 | COLOR_MODE_BRIGHTNESS, 8 | COLOR_MODE_COLOR_TEMP, 9 | LightEntity, 10 | ) 11 | from homeassistant.config_entries import ConfigEntry 12 | from homeassistant.core import HomeAssistant, callback 13 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 14 | from homeassistant.helpers.entity import EntityCategory 15 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 16 | 17 | from .common import VeSyncDevice, has_feature 18 | from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_FAN_TYPES, VS_LIGHTS 19 | 20 | _LOGGER = logging.getLogger(__name__) 21 | 22 | 23 | async def async_setup_entry( 24 | hass: HomeAssistant, 25 | config_entry: ConfigEntry, 26 | async_add_entities: AddEntitiesCallback, 27 | ) -> None: 28 | """Set up lights.""" 29 | 30 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 31 | 32 | @callback 33 | def discover(devices): 34 | """Add new devices to platform.""" 35 | _setup_entities(devices, async_add_entities, coordinator) 36 | 37 | config_entry.async_on_unload( 38 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_LIGHTS), discover) 39 | ) 40 | 41 | _setup_entities( 42 | hass.data[DOMAIN][config_entry.entry_id][VS_LIGHTS], 43 | async_add_entities, 44 | coordinator, 45 | ) 46 | 47 | 48 | @callback 49 | def _setup_entities(devices, async_add_entities, coordinator): 50 | """Check if device is online and add entity.""" 51 | entities = [] 52 | for dev in devices: 53 | if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"): 54 | entities.append(VeSyncDimmableLightHA(dev, coordinator)) 55 | if DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white",): 56 | entities.append(VeSyncTunableWhiteLightHA(dev, coordinator)) 57 | if hasattr(dev, "night_light") and dev.night_light: 58 | entities.append(VeSyncNightLightHA(dev, coordinator)) 59 | 60 | async_add_entities(entities, update_before_add=True) 61 | 62 | 63 | def _vesync_brightness_to_ha(vesync_brightness): 64 | try: 65 | # check for validity of brightness value received 66 | brightness_value = int(vesync_brightness) 67 | except ValueError: 68 | # deal if any unexpected/non numeric value 69 | _LOGGER.debug( 70 | "VeSync - received unexpected 'brightness' value from pyvesync api: %s", 71 | vesync_brightness, 72 | ) 73 | return None 74 | # convert percent brightness to ha expected range 75 | return round((max(1, brightness_value) / 100) * 255) 76 | 77 | 78 | def _ha_brightness_to_vesync(ha_brightness): 79 | # get brightness from HA data 80 | brightness = int(ha_brightness) 81 | # ensure value between 1-255 82 | brightness = max(1, min(brightness, 255)) 83 | # convert to percent that vesync api expects 84 | brightness = round((brightness / 255) * 100) 85 | return max(1, min(brightness, 100)) 86 | 87 | 88 | class VeSyncBaseLight(VeSyncDevice, LightEntity): 89 | """Base class for VeSync Light Devices Representations.""" 90 | 91 | def __init_(self, light, coordinator): 92 | """Initialize the VeSync light device.""" 93 | super().__init__(light, coordinator) 94 | 95 | @property 96 | def brightness(self): 97 | """Get light brightness.""" 98 | # get value from pyvesync library api, 99 | return _vesync_brightness_to_ha(self.device.brightness) 100 | 101 | def turn_on(self, **kwargs): 102 | """Turn the device on.""" 103 | attribute_adjustment_only = False 104 | # set white temperature 105 | if self.color_mode in (COLOR_MODE_COLOR_TEMP,) and ATTR_COLOR_TEMP in kwargs: 106 | # get white temperature from HA data 107 | color_temp = int(kwargs[ATTR_COLOR_TEMP]) 108 | # ensure value between min-max supported Mireds 109 | color_temp = max(self.min_mireds, min(color_temp, self.max_mireds)) 110 | # convert Mireds to Percent value that api expects 111 | color_temp = round( 112 | ((color_temp - self.min_mireds) / (self.max_mireds - self.min_mireds)) 113 | * 100 114 | ) 115 | # flip cold/warm to what pyvesync api expects 116 | color_temp = 100 - color_temp 117 | # ensure value between 0-100 118 | color_temp = max(0, min(color_temp, 100)) 119 | # call pyvesync library api method to set color_temp 120 | self.device.set_color_temp(color_temp) 121 | # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly 122 | attribute_adjustment_only = True 123 | # set brightness level 124 | if ( 125 | self.color_mode in (COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP) 126 | and ATTR_BRIGHTNESS in kwargs 127 | ): 128 | # get brightness from HA data 129 | brightness = _ha_brightness_to_vesync(kwargs[ATTR_BRIGHTNESS]) 130 | self.device.set_brightness(brightness) 131 | # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly 132 | attribute_adjustment_only = True 133 | # check flag if should skip sending the turn_on command 134 | if attribute_adjustment_only: 135 | return 136 | # send turn_on command to pyvesync api 137 | self.device.turn_on() 138 | 139 | 140 | class VeSyncDimmableLightHA(VeSyncBaseLight, LightEntity): 141 | """Representation of a VeSync dimmable light device.""" 142 | 143 | def __init__(self, device, coordinator) -> None: 144 | """Initialize the VeSync dimmable light device.""" 145 | super().__init__(device, coordinator) 146 | 147 | @property 148 | def color_mode(self): 149 | """Set color mode for this entity.""" 150 | return COLOR_MODE_BRIGHTNESS 151 | 152 | @property 153 | def supported_color_modes(self): 154 | """Flag supported color_modes (in an array format).""" 155 | return [COLOR_MODE_BRIGHTNESS] 156 | 157 | 158 | class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): 159 | """Representation of a VeSync Tunable White Light device.""" 160 | 161 | def __init__(self, device, coordinator) -> None: 162 | """Initialize the VeSync Tunable White Light device.""" 163 | super().__init__(device, coordinator) 164 | 165 | @property 166 | def color_temp(self): 167 | """Get device white temperature.""" 168 | # get value from pyvesync library api, 169 | result = self.device.color_temp_pct 170 | try: 171 | # check for validity of brightness value received 172 | color_temp_value = int(result) 173 | except ValueError: 174 | # deal if any unexpected/non numeric value 175 | _LOGGER.debug( 176 | "VeSync - received unexpected 'color_temp_pct' value from pyvesync api: %s", 177 | result, 178 | ) 179 | return 0 180 | # flip cold/warm 181 | color_temp_value = 100 - color_temp_value 182 | # ensure value between 0-100 183 | color_temp_value = max(0, min(color_temp_value, 100)) 184 | # convert percent value to Mireds 185 | color_temp_value = round( 186 | self.min_mireds 187 | + ((self.max_mireds - self.min_mireds) / 100 * color_temp_value) 188 | ) 189 | # ensure value between minimum and maximum Mireds 190 | return max(self.min_mireds, min(color_temp_value, self.max_mireds)) 191 | 192 | @property 193 | def min_mireds(self): 194 | """Set device coldest white temperature.""" 195 | return 154 # 154 Mireds ( 1,000,000 divided by 6500 Kelvin = 154 Mireds) 196 | 197 | @property 198 | def max_mireds(self): 199 | """Set device warmest white temperature.""" 200 | return 370 # 370 Mireds ( 1,000,000 divided by 2700 Kelvin = 370 Mireds) 201 | 202 | @property 203 | def color_mode(self): 204 | """Set color mode for this entity.""" 205 | return COLOR_MODE_COLOR_TEMP 206 | 207 | @property 208 | def supported_color_modes(self): 209 | """Flag supported color_modes (in an array format).""" 210 | return [COLOR_MODE_COLOR_TEMP] 211 | 212 | 213 | class VeSyncNightLightHA(VeSyncDimmableLightHA): 214 | """Representation of the night light on a VeSync device.""" 215 | 216 | def __init__(self, device, coordinator) -> None: 217 | """Initialize the VeSync device.""" 218 | super().__init__(device, coordinator) 219 | self.device = device 220 | self.has_brightness = has_feature( 221 | self.device, "details", "night_light_brightness" 222 | ) 223 | 224 | @property 225 | def unique_id(self): 226 | """Return the ID of this device.""" 227 | return f"{super().unique_id}-night-light" 228 | 229 | @property 230 | def name(self): 231 | """Return the name of the device.""" 232 | return f"{super().name} night light" 233 | 234 | @property 235 | def brightness(self): 236 | """Get night light brightness.""" 237 | return ( 238 | _vesync_brightness_to_ha(self.device.details["night_light_brightness"]) 239 | if self.has_brightness 240 | else {"on": 255, "dim": 125, "off": 0}[self.device.details["night_light"]] 241 | ) 242 | 243 | @property 244 | def is_on(self): 245 | """Return True if night light is on.""" 246 | if has_feature(self.device, "details", "night_light"): 247 | return self.device.details["night_light"] in ["on", "dim"] 248 | if self.has_brightness: 249 | return self.device.details["night_light_brightness"] > 0 250 | 251 | @property 252 | def entity_category(self): 253 | """Return the configuration entity category.""" 254 | return EntityCategory.CONFIG 255 | 256 | def turn_on(self, **kwargs): 257 | """Turn the night light on.""" 258 | if self.device._config_dict["module"] in VS_FAN_TYPES: 259 | if ATTR_BRIGHTNESS in kwargs and kwargs[ATTR_BRIGHTNESS] < 255: 260 | self.device.set_night_light("dim") 261 | else: 262 | self.device.set_night_light("on") 263 | elif ATTR_BRIGHTNESS in kwargs: 264 | self.device.set_night_light_brightness( 265 | _ha_brightness_to_vesync(kwargs[ATTR_BRIGHTNESS]) 266 | ) 267 | else: 268 | self.device.set_night_light_brightness(100) 269 | 270 | def turn_off(self, **kwargs): 271 | """Turn the night light off.""" 272 | if self.device._config_dict["module"] in VS_FAN_TYPES: 273 | self.device.set_night_light("off") 274 | else: 275 | self.device.set_night_light_brightness(0) 276 | -------------------------------------------------------------------------------- /custom_components/vesync/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "vesync", 3 | "name": "VeSync", 4 | "codeowners": [ 5 | "@markperdue", 6 | "@webdjoe", 7 | "@thegardenmonkey", 8 | "@vlebourl", 9 | "@tv4you2016" 10 | ], 11 | "config_flow": true, 12 | "dhcp": [ 13 | { 14 | "hostname": "levoit-*", 15 | "macaddress": "*" 16 | } 17 | ], 18 | "documentation": "https://github.com/micahqcade/custom_vesync", 19 | "iot_class": "cloud_polling", 20 | "issue_tracker": "https://github.com/micahqcade/custom_vesync", 21 | "requirements": [ 22 | "pyvesync==2.1.12" 23 | ], 24 | "version": "1.3.2" 25 | } -------------------------------------------------------------------------------- /custom_components/vesync/number.py: -------------------------------------------------------------------------------- 1 | """Support for number settings on VeSync devices.""" 2 | 3 | from homeassistant.components.number import NumberEntity 4 | from homeassistant.components.sensor import SensorDeviceClass 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.const import PERCENTAGE 7 | from homeassistant.core import HomeAssistant, callback 8 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 9 | from homeassistant.helpers.entity import EntityCategory 10 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 11 | 12 | from .common import VeSyncBaseEntity, has_feature 13 | from .const import DOMAIN, VS_DISCOVERY, VS_NUMBERS 14 | 15 | MAX_HUMIDITY = 80 16 | MIN_HUMIDITY = 30 17 | 18 | 19 | async def async_setup_entry( 20 | hass: HomeAssistant, 21 | config_entry: ConfigEntry, 22 | async_add_entities: AddEntitiesCallback, 23 | ) -> None: 24 | """Set up numbers.""" 25 | 26 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 27 | 28 | @callback 29 | def discover(devices): 30 | """Add new devices to platform.""" 31 | _setup_entities(devices, async_add_entities, coordinator) 32 | 33 | config_entry.async_on_unload( 34 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_NUMBERS), discover) 35 | ) 36 | 37 | _setup_entities( 38 | hass.data[DOMAIN][config_entry.entry_id][VS_NUMBERS], 39 | async_add_entities, 40 | coordinator, 41 | ) 42 | 43 | 44 | @callback 45 | def _setup_entities(devices, async_add_entities, coordinator): 46 | """Check if device is online and add entity.""" 47 | entities = [] 48 | for dev in devices: 49 | if has_feature(dev, "details", "mist_virtual_level"): 50 | entities.append(VeSyncHumidifierMistLevelHA(dev, coordinator)) 51 | if has_feature(dev, "config", "auto_target_humidity"): 52 | entities.append(VeSyncHumidifierTargetLevelHA(dev, coordinator)) 53 | if has_feature(dev, "details", "warm_mist_level"): 54 | entities.append(VeSyncHumidifierWarmthLevelHA(dev, coordinator)) 55 | if has_feature(dev, "_config_dict", "levels"): 56 | entities.append(VeSyncFanSpeedLevelHA(dev, coordinator)) 57 | 58 | async_add_entities(entities, update_before_add=True) 59 | 60 | 61 | class VeSyncNumberEntity(VeSyncBaseEntity, NumberEntity): 62 | """Representation of a number for configuring a VeSync fan.""" 63 | 64 | def __init__(self, device, coordinator) -> None: 65 | """Initialize the VeSync fan device.""" 66 | super().__init__(device, coordinator) 67 | 68 | @property 69 | def entity_category(self): 70 | """Return the diagnostic entity category.""" 71 | return EntityCategory.CONFIG 72 | 73 | 74 | class VeSyncFanSpeedLevelHA(VeSyncNumberEntity): 75 | """Representation of the fan speed level of a VeSync fan.""" 76 | 77 | def __init__(self, device, coordinator) -> None: 78 | """Initialize the number entity.""" 79 | super().__init__(device, coordinator) 80 | self._attr_native_min_value = device._config_dict["levels"][0] 81 | self._attr_native_max_value = device._config_dict["levels"][-1] 82 | self._attr_native_step = 1 83 | 84 | @property 85 | def unique_id(self): 86 | """Return the ID of this device.""" 87 | return f"{super().unique_id}-fan-speed-level" 88 | 89 | @property 90 | def name(self): 91 | """Return the name of the device.""" 92 | return f"{super().name} fan speed level" 93 | 94 | @property 95 | def native_value(self): 96 | """Return the fan speed level.""" 97 | return self.device.speed 98 | 99 | @property 100 | def extra_state_attributes(self): 101 | """Return the state attributes of the humidifier.""" 102 | return {"fan speed levels": self.device._config_dict["levels"]} 103 | 104 | def set_native_value(self, value): 105 | """Set the fan speed level.""" 106 | self.device.change_fan_speed(int(value)) 107 | 108 | 109 | class VeSyncHumidifierMistLevelHA(VeSyncNumberEntity): 110 | """Representation of the mist level of a VeSync humidifier.""" 111 | 112 | def __init__(self, device, coordinator) -> None: 113 | """Initialize the number entity.""" 114 | super().__init__(device, coordinator) 115 | self._attr_native_min_value = device._config_dict["mist_levels"][0] 116 | self._attr_native_max_value = device._config_dict["mist_levels"][-1] 117 | self._attr_native_step = 1 118 | 119 | @property 120 | def unique_id(self): 121 | """Return the ID of this device.""" 122 | return f"{super().unique_id}-mist-level" 123 | 124 | @property 125 | def name(self): 126 | """Return the name of the device.""" 127 | return f"{super().name} mist level" 128 | 129 | @property 130 | def native_value(self): 131 | """Return the mist level.""" 132 | return self.device.details["mist_virtual_level"] 133 | 134 | @property 135 | def extra_state_attributes(self): 136 | """Return the state attributes of the humidifier.""" 137 | return {"mist levels": self.device._config_dict["mist_levels"]} 138 | 139 | def set_native_value(self, value): 140 | """Set the mist level.""" 141 | self.device.set_mist_level(int(value)) 142 | 143 | 144 | class VeSyncHumidifierWarmthLevelHA(VeSyncNumberEntity): 145 | """Representation of the warmth level of a VeSync humidifier.""" 146 | 147 | def __init__(self, device, coordinator) -> None: 148 | """Initialize the number entity.""" 149 | super().__init__(device, coordinator) 150 | self._attr_native_min_value = device._config_dict["warm_mist_levels"][0] 151 | self._attr_native_max_value = device._config_dict["warm_mist_levels"][-1] 152 | self._attr_native_step = 1 153 | 154 | @property 155 | def unique_id(self): 156 | """Return the ID of this device.""" 157 | return f"{super().unique_id}-warm-mist" 158 | 159 | @property 160 | def name(self): 161 | """Return the name of the device.""" 162 | return f"{super().name} warm mist" 163 | 164 | @property 165 | def native_value(self): 166 | """Return the warmth level.""" 167 | return self.device.details["warm_mist_level"] 168 | 169 | @property 170 | def extra_state_attributes(self): 171 | """Return the state attributes of the humidifier.""" 172 | return {"warm mist levels": self.device._config_dict["warm_mist_levels"]} 173 | 174 | def set_native_value(self, value): 175 | """Set the mist level.""" 176 | self.device.set_warm_level(int(value)) 177 | 178 | 179 | class VeSyncHumidifierTargetLevelHA(VeSyncNumberEntity): 180 | """Representation of the target humidity level of a VeSync humidifier.""" 181 | 182 | def __init__(self, device, coordinator) -> None: 183 | """Initialize the number entity.""" 184 | super().__init__(device, coordinator) 185 | self._attr_native_min_value = MIN_HUMIDITY 186 | self._attr_native_max_value = MAX_HUMIDITY 187 | self._attr_native_step = 1 188 | 189 | @property 190 | def unique_id(self): 191 | """Return the ID of this device.""" 192 | return f"{super().unique_id}-target-level" 193 | 194 | @property 195 | def name(self): 196 | """Return the name of the device.""" 197 | return f"{super().name} target level" 198 | 199 | @property 200 | def native_value(self): 201 | """Return the current target humidity level.""" 202 | return self.device.config["auto_target_humidity"] 203 | 204 | @property 205 | def native_unit_of_measurement(self): 206 | """Return the native unit of measurement for the target humidity level.""" 207 | return PERCENTAGE 208 | 209 | @property 210 | def device_class(self): 211 | """Return the device class of the target humidity level. 212 | 213 | Eventually this should become NumberDeviceClass but that was introduced in 2022.12. 214 | For maximum compatibility, using SensorDeviceClass as recommended by deprecation notice. 215 | Or hard code this to "humidity" 216 | """ 217 | 218 | return SensorDeviceClass.HUMIDITY 219 | 220 | def set_native_value(self, value): 221 | """Set the target humidity level.""" 222 | self.device.set_humidity(int(value)) 223 | -------------------------------------------------------------------------------- /custom_components/vesync/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for power & energy sensors for VeSync outlets.""" 2 | 3 | import logging 4 | 5 | from homeassistant.components.sensor import ( 6 | SensorDeviceClass, 7 | SensorEntity, 8 | SensorStateClass, 9 | ) 10 | from homeassistant.config_entries import ConfigEntry 11 | from homeassistant.const import ( 12 | CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, 13 | DEGREE, 14 | PERCENTAGE, 15 | UnitOfEnergy, 16 | UnitOfPower, 17 | ) 18 | from homeassistant.core import HomeAssistant, callback 19 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 20 | from homeassistant.helpers.entity import EntityCategory 21 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 22 | 23 | from .common import VeSyncBaseEntity, has_feature 24 | from .const import ( 25 | DEV_TYPE_TO_HA, 26 | DOMAIN, 27 | SENSOR_TYPES_AIRFRYER, 28 | VS_DISCOVERY, 29 | VS_SENSORS, 30 | ) 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | 35 | async def async_setup_entry( 36 | hass: HomeAssistant, 37 | config_entry: ConfigEntry, 38 | async_add_entities: AddEntitiesCallback, 39 | ) -> None: 40 | """Set up switches.""" 41 | 42 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 43 | 44 | @callback 45 | def discover(devices): 46 | """Add new devices to platform.""" 47 | _setup_entities(devices, async_add_entities, coordinator) 48 | 49 | config_entry.async_on_unload( 50 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SENSORS), discover) 51 | ) 52 | 53 | _setup_entities( 54 | hass.data[DOMAIN][config_entry.entry_id][VS_SENSORS], 55 | async_add_entities, 56 | coordinator, 57 | ) 58 | 59 | 60 | @callback 61 | def _setup_entities(devices, async_add_entities, coordinator): 62 | """Check if device is online and add entity.""" 63 | entities = [] 64 | for dev in devices: 65 | if hasattr(dev, "fryer_status"): 66 | for stype in SENSOR_TYPES_AIRFRYER.values(): 67 | entities.append( # noqa: PERF401 68 | VeSyncairfryerSensor( 69 | dev, 70 | coordinator, 71 | stype, 72 | ) 73 | ) 74 | 75 | if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": 76 | entities.extend( 77 | ( 78 | VeSyncPowerSensor(dev, coordinator), 79 | VeSyncEnergySensor(dev, coordinator), 80 | ) 81 | ) 82 | if has_feature(dev, "details", "humidity"): 83 | entities.append(VeSyncHumiditySensor(dev, coordinator)) 84 | if has_feature(dev, "details", "air_quality"): 85 | entities.append(VeSyncAirQualitySensor(dev, coordinator)) 86 | if has_feature(dev, "details", "aq_percent"): 87 | entities.append(VeSyncAirQualityPercSensor(dev, coordinator)) 88 | if has_feature(dev, "details", "air_quality_value"): 89 | entities.append(VeSyncAirQualityValueSensor(dev, coordinator)) 90 | if has_feature(dev, "details", "pm1"): 91 | entities.append(VeSyncPM1Sensor(dev, coordinator)) 92 | if has_feature(dev, "details", "pm10"): 93 | entities.append(VeSyncPM10Sensor(dev, coordinator)) 94 | if has_feature(dev, "details", "filter_life"): 95 | entities.append(VeSyncFilterLifeSensor(dev, coordinator)) 96 | if has_feature(dev, "details", "fan_rotate_angle"): 97 | entities.append(VeSyncFanRotateAngleSensor(dev, coordinator)) 98 | 99 | async_add_entities(entities, update_before_add=True) 100 | 101 | 102 | class VeSyncairfryerSensor(VeSyncBaseEntity, SensorEntity): 103 | """Class representing a VeSyncairfryerSensor.""" 104 | 105 | def __init__(self, airfryer, coordinator, stype) -> None: 106 | """Initialize the VeSync airfryer.""" 107 | super().__init__(airfryer, coordinator) 108 | self.airfryer = airfryer 109 | self.stype = stype 110 | 111 | @property 112 | def unique_id(self): 113 | """Return unique ID for power sensor on device.""" 114 | return f"{super().unique_id}-" + self.stype[0] 115 | 116 | @property 117 | def name(self): 118 | """Return sensor name.""" 119 | return self.stype[1] 120 | 121 | @property 122 | def device_class(self): 123 | """Return the class.""" 124 | return self.stype[4] 125 | 126 | @property 127 | def native_value(self): 128 | """Return the value.""" 129 | return getattr(self.airfryer, self.stype[5], None) 130 | 131 | @property 132 | def native_unit_of_measurement(self): 133 | """Return the unit of measurement.""" 134 | # return self.airfryer.temp_unit 135 | return self.stype[2] 136 | 137 | @property 138 | def icon(self): 139 | """Return the icon to use in the frontend, if any.""" 140 | return self.stype[3] 141 | 142 | 143 | class VeSyncOutletSensorEntity(VeSyncBaseEntity, SensorEntity): 144 | """Representation of a sensor describing diagnostics of a VeSync outlet.""" 145 | 146 | def __init__(self, plug, coordinator) -> None: 147 | """Initialize the VeSync outlet device.""" 148 | super().__init__(plug, coordinator) 149 | self.smartplug = plug 150 | 151 | @property 152 | def entity_category(self): 153 | """Return the diagnostic entity category.""" 154 | return EntityCategory.DIAGNOSTIC 155 | 156 | 157 | class VeSyncPowerSensor(VeSyncOutletSensorEntity): 158 | """Representation of current power use for a VeSync outlet.""" 159 | 160 | def __init__(self, plug, coordinator) -> None: 161 | """Initialize the VeSync outlet device.""" 162 | super().__init__(plug, coordinator) 163 | 164 | @property 165 | def unique_id(self): 166 | """Return unique ID for power sensor on device.""" 167 | return f"{super().unique_id}-power" 168 | 169 | @property 170 | def name(self): 171 | """Return sensor name.""" 172 | return f"{super().name} current power" 173 | 174 | @property 175 | def device_class(self): 176 | """Return the power device class.""" 177 | return SensorDeviceClass.POWER 178 | 179 | @property 180 | def native_value(self): 181 | """Return the current power usage in W.""" 182 | return self.smartplug.power 183 | 184 | @property 185 | def native_unit_of_measurement(self): 186 | """Return the Watt unit of measurement.""" 187 | return UnitOfPower.WATT 188 | 189 | @property 190 | def state_class(self): 191 | """Return the measurement state class.""" 192 | return SensorStateClass.MEASUREMENT 193 | 194 | def update(self): 195 | """Update outlet details and energy usage.""" 196 | self.smartplug.update() 197 | self.smartplug.update_energy() 198 | 199 | 200 | class VeSyncEnergySensor(VeSyncOutletSensorEntity): 201 | """Representation of current day's energy use for a VeSync outlet.""" 202 | 203 | def __init__(self, plug, coordinator) -> None: 204 | """Initialize the VeSync outlet device.""" 205 | super().__init__(plug, coordinator) 206 | self.smartplug = plug 207 | 208 | @property 209 | def unique_id(self): 210 | """Return unique ID for power sensor on device.""" 211 | return f"{super().unique_id}-energy" 212 | 213 | @property 214 | def name(self): 215 | """Return sensor name.""" 216 | return f"{super().name} energy use today" 217 | 218 | @property 219 | def device_class(self): 220 | """Return the energy device class.""" 221 | return SensorDeviceClass.ENERGY 222 | 223 | @property 224 | def native_value(self): 225 | """Return the today total energy usage in kWh.""" 226 | return self.smartplug.energy_today 227 | 228 | @property 229 | def native_unit_of_measurement(self): 230 | """Return the kWh unit of measurement.""" 231 | return UnitOfEnergy.KILO_WATT_HOUR 232 | 233 | @property 234 | def state_class(self): 235 | """Return the total_increasing state class.""" 236 | return SensorStateClass.TOTAL_INCREASING 237 | 238 | def update(self): 239 | """Update outlet details and energy usage.""" 240 | self.smartplug.update() 241 | self.smartplug.update_energy() 242 | 243 | 244 | class VeSyncHumidifierSensorEntity(VeSyncBaseEntity, SensorEntity): 245 | """Representation of a sensor describing diagnostics of a VeSync humidifier.""" 246 | 247 | def __init__(self, humidifier, coordinator) -> None: 248 | """Initialize the VeSync humidifier device.""" 249 | super().__init__(humidifier, coordinator) 250 | self.smarthumidifier = humidifier 251 | 252 | @property 253 | def entity_category(self): 254 | """Return the diagnostic entity category.""" 255 | return None 256 | 257 | 258 | class VeSyncAirQualitySensor(VeSyncHumidifierSensorEntity): 259 | """Representation of an air quality sensor.""" 260 | 261 | _attr_state_class = SensorStateClass.MEASUREMENT 262 | 263 | def __init__(self, device, coordinator) -> None: 264 | """Initialize the VeSync device.""" 265 | super().__init__(device, coordinator) 266 | self._numeric_quality = None 267 | if self.native_value is not None: 268 | self._numeric_quality = isinstance(self.native_value, (int, float)) 269 | 270 | @property 271 | def device_class(self): 272 | """Return the air quality device class.""" 273 | return SensorDeviceClass.AQI if self._numeric_quality else None 274 | 275 | @property 276 | def unique_id(self): 277 | """Return unique ID for air quality sensor on device.""" 278 | return f"{super().unique_id}-air-quality" 279 | 280 | @property 281 | def name(self): 282 | """Return sensor name.""" 283 | return f"{super().name} air quality" 284 | 285 | @property 286 | def native_value(self): 287 | """Return the air quality index.""" 288 | if has_feature(self.smarthumidifier, "details", "air_quality"): 289 | quality = self.smarthumidifier.details["air_quality"] 290 | if isinstance(quality, (int, float)): 291 | return quality 292 | _LOGGER.warning( 293 | "Got non numeric value for AQI sensor from 'air_quality' for %s: %s", 294 | self.name, 295 | quality, 296 | ) 297 | _LOGGER.warning("No air quality index found in '%s'", self.name) 298 | return None 299 | 300 | 301 | class VeSyncAirQualityPercSensor(VeSyncHumidifierSensorEntity): 302 | """Representation of an air quality percentage sensor.""" 303 | 304 | _attr_state_class = SensorStateClass.MEASUREMENT 305 | 306 | def __init__(self, device, coordinator) -> None: 307 | """Initialize the VeSync device.""" 308 | super().__init__(device, coordinator) 309 | self._numeric_quality = None 310 | if self.native_value is not None: 311 | self._numeric_quality = isinstance(self.native_value, (int, float)) 312 | 313 | @property 314 | def unique_id(self): 315 | """Return unique ID for air quality sensor on device.""" 316 | return f"{super().unique_id}-air-quality-perc" 317 | 318 | @property 319 | def name(self): 320 | """Return sensor name.""" 321 | return f"{super().name} air quality percentage" 322 | 323 | @property 324 | def native_unit_of_measurement(self): 325 | """Return the % unit of measurement.""" 326 | return PERCENTAGE 327 | 328 | @property 329 | def native_value(self): 330 | """Return the air quality percentage.""" 331 | if has_feature(self.smarthumidifier, "details", "aq_percent"): 332 | quality = self.smarthumidifier.details["aq_percent"] 333 | if isinstance(quality, (int, float)): 334 | return quality 335 | _LOGGER.warning( 336 | "Got non numeric value for AQI sensor from 'aq_percent' for %s: %s", 337 | self.name, 338 | quality, 339 | ) 340 | _LOGGER.warning("No air quality percentage found in '%s'", self.name) 341 | return None 342 | 343 | 344 | class VeSyncAirQualityValueSensor(VeSyncHumidifierSensorEntity): 345 | """Representation of an air quality sensor.""" 346 | 347 | _attr_state_class = SensorStateClass.MEASUREMENT 348 | _attr_device_class = SensorDeviceClass.PM25 349 | _attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER 350 | 351 | def __init__(self, device, coordinator) -> None: 352 | """Initialize the VeSync device.""" 353 | super().__init__(device, coordinator) 354 | 355 | @property 356 | def unique_id(self): 357 | """Return unique ID for air quality sensor on device.""" 358 | return f"{super().unique_id}-air-quality-value" 359 | 360 | @property 361 | def name(self): 362 | """Return sensor name.""" 363 | return f"{super().name} air quality value" 364 | 365 | @property 366 | def native_value(self): 367 | """Return the air quality index.""" 368 | if has_feature(self.smarthumidifier, "details", "air_quality_value"): 369 | quality_value = self.smarthumidifier.details["air_quality_value"] 370 | if isinstance(quality_value, (int, float)): 371 | return quality_value 372 | _LOGGER.warning( 373 | "Got non numeric value for AQI sensor from 'air_quality_value' for %s: %s", 374 | self.name, 375 | quality_value, 376 | ) 377 | _LOGGER.warning("No air quality value found in '%s'", self.name) 378 | return None 379 | 380 | 381 | class VeSyncPM1Sensor(VeSyncHumidifierSensorEntity): 382 | """Representation of a PM1 sensor.""" 383 | 384 | _attr_state_class = SensorStateClass.MEASUREMENT 385 | _attr_device_class = SensorDeviceClass.PM1 386 | _attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER 387 | 388 | def __init__(self, device, coordinator) -> None: 389 | """Initialize the VeSync device.""" 390 | super().__init__(device, coordinator) 391 | 392 | @property 393 | def unique_id(self): 394 | """Return unique ID for PM1 sensor on device.""" 395 | return f"{super().unique_id}-pm1" 396 | 397 | @property 398 | def name(self): 399 | """Return sensor name.""" 400 | return f"{super().name} PM1" 401 | 402 | @property 403 | def native_value(self): 404 | """Return the PM1.""" 405 | if has_feature(self.smarthumidifier, "details", "pm1"): 406 | quality_value = self.smarthumidifier.details["pm1"] 407 | if isinstance(quality_value, (int, float)): 408 | return quality_value 409 | _LOGGER.warning( 410 | "Got non numeric value for PM1 sensor from 'pm1' for %s: %s", 411 | self.name, 412 | quality_value, 413 | ) 414 | _LOGGER.warning("No PM1 value found in '%s'", self.name) 415 | return None 416 | 417 | 418 | class VeSyncPM10Sensor(VeSyncHumidifierSensorEntity): 419 | """Representation of a PM10 sensor.""" 420 | 421 | _attr_state_class = SensorStateClass.MEASUREMENT 422 | _attr_device_class = SensorDeviceClass.PM10 423 | _attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER 424 | 425 | def __init__(self, device, coordinator) -> None: 426 | """Initialize the VeSync device.""" 427 | super().__init__(device, coordinator) 428 | 429 | @property 430 | def unique_id(self): 431 | """Return unique ID for PM10 sensor on device.""" 432 | return f"{super().unique_id}-pm10" 433 | 434 | @property 435 | def name(self): 436 | """Return sensor name.""" 437 | return f"{super().name} PM10" 438 | 439 | @property 440 | def native_value(self): 441 | """Return the PM10.""" 442 | if has_feature(self.smarthumidifier, "details", "pm10"): 443 | quality_value = self.smarthumidifier.details["pm10"] 444 | if isinstance(quality_value, (int, float)): 445 | return quality_value 446 | _LOGGER.warning( 447 | "Got non numeric value for PM10 sensor from 'pm10' for %s: %s", 448 | self.name, 449 | quality_value, 450 | ) 451 | _LOGGER.warning("No PM10 value found in '%s'", self.name) 452 | return None 453 | 454 | 455 | class VeSyncFilterLifeSensor(VeSyncHumidifierSensorEntity): 456 | """Representation of a filter life sensor.""" 457 | 458 | def __init__(self, plug, coordinator) -> None: 459 | """Initialize the VeSync device.""" 460 | super().__init__(plug, coordinator) 461 | 462 | @property 463 | def entity_category(self): 464 | """Return the diagnostic entity category.""" 465 | return EntityCategory.DIAGNOSTIC 466 | 467 | @property 468 | def unique_id(self): 469 | """Return unique ID for filter life sensor on device.""" 470 | return f"{super().unique_id}-filter-life" 471 | 472 | @property 473 | def name(self): 474 | """Return sensor name.""" 475 | return f"{super().name} filter life" 476 | 477 | @property 478 | def device_class(self): 479 | """Return the filter life device class.""" 480 | return None 481 | 482 | @property 483 | def native_value(self): 484 | """Return the filter life index.""" 485 | return ( 486 | self.smarthumidifier.filter_life 487 | if hasattr(self.smarthumidifier, "filter_life") 488 | else self.smarthumidifier.details["filter_life"] 489 | ) 490 | 491 | @property 492 | def native_unit_of_measurement(self): 493 | """Return the % unit of measurement.""" 494 | return PERCENTAGE 495 | 496 | @property 497 | def state_class(self): 498 | """Return the measurement state class.""" 499 | return SensorStateClass.MEASUREMENT 500 | 501 | @property 502 | def state_attributes(self): 503 | """Return the state attributes.""" 504 | return ( 505 | self.smarthumidifier.details["filter_life"] 506 | if isinstance(self.smarthumidifier.details["filter_life"], dict) 507 | else {} 508 | ) 509 | 510 | @property 511 | def icon(self): 512 | """Return the icon to use in the frontend, if any.""" 513 | return "mdi:air-filter" 514 | 515 | 516 | class VeSyncFanRotateAngleSensor(VeSyncHumidifierSensorEntity): 517 | """Representation of a fan rotate angle sensor.""" 518 | 519 | def __init__(self, plug, coordinator) -> None: 520 | """Initialize the VeSync device.""" 521 | super().__init__(plug, coordinator) 522 | 523 | @property 524 | def entity_category(self): 525 | """Return the diagnostic entity category.""" 526 | return EntityCategory.DIAGNOSTIC 527 | 528 | @property 529 | def unique_id(self): 530 | """Return unique ID for filter life sensor on device.""" 531 | return f"{super().unique_id}-fan-rotate-angle" 532 | 533 | @property 534 | def name(self): 535 | """Return sensor name.""" 536 | return f"{super().name} fan rotate angle" 537 | 538 | @property 539 | def device_class(self): 540 | """Return the fan rotate angle device class.""" 541 | return None 542 | 543 | @property 544 | def native_value(self): 545 | """Return the fan rotate angle index.""" 546 | return ( 547 | self.smarthumidifier.fan_rotate_angle 548 | if hasattr(self.smarthumidifier, "fan_rotate_angle") 549 | else self.smarthumidifier.details["fan_rotate_angle"] 550 | ) 551 | 552 | @property 553 | def native_unit_of_measurement(self): 554 | """Return the % unit of measurement.""" 555 | return DEGREE 556 | 557 | @property 558 | def state_class(self): 559 | """Return the measurement state class.""" 560 | return SensorStateClass.MEASUREMENT 561 | 562 | @property 563 | def icon(self): 564 | """Return the icon to use in the frontend, if any.""" 565 | return "mdi:rotate-3d-variant" 566 | 567 | 568 | class VeSyncHumiditySensor(VeSyncHumidifierSensorEntity): 569 | """Representation of current humidity for a VeSync humidifier.""" 570 | 571 | def __init__(self, humidity, coordinator) -> None: 572 | """Initialize the VeSync outlet device.""" 573 | super().__init__(humidity, coordinator) 574 | 575 | @property 576 | def unique_id(self): 577 | """Return unique ID for humidity sensor on device.""" 578 | return f"{super().unique_id}-humidity" 579 | 580 | @property 581 | def name(self): 582 | """Return sensor name.""" 583 | return f"{super().name} current humidity" 584 | 585 | @property 586 | def device_class(self): 587 | """Return the humidity device class.""" 588 | return SensorDeviceClass.HUMIDITY 589 | 590 | @property 591 | def native_value(self): 592 | """Return the current humidity in percent.""" 593 | return self.smarthumidifier.details["humidity"] 594 | 595 | @property 596 | def native_unit_of_measurement(self): 597 | """Return the % unit of measurement.""" 598 | return PERCENTAGE 599 | 600 | @property 601 | def state_class(self): 602 | """Return the measurement state class.""" 603 | return SensorStateClass.MEASUREMENT 604 | -------------------------------------------------------------------------------- /custom_components/vesync/services.yaml: -------------------------------------------------------------------------------- 1 | update_devices: 2 | name: Update devices 3 | description: Add new VeSync devices to Home Assistant 4 | -------------------------------------------------------------------------------- /custom_components/vesync/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "VeSync Integration for Home Assistant", 3 | "device_automation": { 4 | "action_type": { 5 | "set_mode": "Change mode on {entity_name}" 6 | } 7 | }, 8 | "config": { 9 | "flow_title": "Gateway: {gateway_id}", 10 | "step": { 11 | "user": { 12 | "title": "VeSync Integration for Home Assistant", 13 | "description": "Custom component for Home Assistant to interact with smart devices via the VeSync platform.", 14 | "data": { 15 | "username": "[%key:common::config_flow::data::email%]", 16 | "password": "[%key:common::config_flow::data::password%]" 17 | } 18 | } 19 | }, 20 | "error": { 21 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" 22 | }, 23 | "abort": { 24 | "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /custom_components/vesync/switch.py: -------------------------------------------------------------------------------- 1 | """Support for VeSync switches.""" 2 | import logging 3 | 4 | from homeassistant.components.switch import SwitchEntity 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import HomeAssistant, callback 7 | from homeassistant.helpers.dispatcher import async_dispatcher_connect 8 | from homeassistant.helpers.entity import EntityCategory 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | 11 | from .common import VeSyncBaseEntity, VeSyncDevice 12 | from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_SWITCHES 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | async def async_setup_entry( 18 | hass: HomeAssistant, 19 | config_entry: ConfigEntry, 20 | async_add_entities: AddEntitiesCallback, 21 | ) -> None: 22 | """Set up switches.""" 23 | 24 | coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] 25 | 26 | @callback 27 | def discover(devices): 28 | """Add new devices to platform.""" 29 | _setup_entities(devices, async_add_entities, coordinator) 30 | 31 | config_entry.async_on_unload( 32 | async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SWITCHES), discover) 33 | ) 34 | 35 | _setup_entities( 36 | hass.data[DOMAIN][config_entry.entry_id][VS_SWITCHES], 37 | async_add_entities, 38 | coordinator, 39 | ) 40 | 41 | 42 | @callback 43 | def _setup_entities(devices, async_add_entities, coordinator): 44 | """Check if device is online and add entity.""" 45 | entities = [] 46 | for dev in devices: 47 | if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": 48 | entities.append(VeSyncSwitchHA(dev, coordinator)) 49 | if DEV_TYPE_TO_HA.get(dev.device_type) == "switch": 50 | entities.append(VeSyncLightSwitch(dev, coordinator)) 51 | if getattr(dev, "set_auto_mode", None): 52 | entities.append(VeSyncHumidifierAutoOnHA(dev, coordinator)) 53 | if getattr(dev, "automatic_stop_on", None): 54 | entities.append(VeSyncHumidifierAutomaticStopHA(dev, coordinator)) 55 | if getattr(dev, "turn_on_display", None): 56 | entities.append(VeSyncHumidifierDisplayHA(dev, coordinator)) 57 | if getattr(dev, "child_lock_on", None): 58 | entities.append(VeSyncFanChildLockHA(dev, coordinator)) 59 | 60 | async_add_entities(entities, update_before_add=True) 61 | 62 | 63 | class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity): 64 | """Base class for VeSync switch Device Representations.""" 65 | 66 | def __init__(self, plug, coordinator) -> None: 67 | """Initialize the VeSync outlet device.""" 68 | super().__init__(plug, coordinator) 69 | 70 | def turn_on(self, **kwargs): 71 | """Turn the device on.""" 72 | self.device.turn_on() 73 | 74 | 75 | class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): 76 | """Representation of a VeSync switch.""" 77 | 78 | def __init__(self, plug, coordinator) -> None: 79 | """Initialize the VeSync switch device.""" 80 | super().__init__(plug, coordinator) 81 | self.smartplug = plug 82 | 83 | @property 84 | def extra_state_attributes(self): 85 | """Return the state attributes of the device.""" 86 | return ( 87 | { 88 | "voltage": self.smartplug.voltage, 89 | "weekly_energy_total": self.smartplug.weekly_energy_total, 90 | "monthly_energy_total": self.smartplug.monthly_energy_total, 91 | "yearly_energy_total": self.smartplug.yearly_energy_total, 92 | } 93 | if hasattr(self.smartplug, "weekly_energy_total") 94 | else {} 95 | ) 96 | 97 | def update(self): 98 | """Update outlet details and energy usage.""" 99 | self.smartplug.update() 100 | self.smartplug.update_energy() 101 | 102 | 103 | class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity): 104 | """Handle representation of VeSync Light Switch.""" 105 | 106 | def __init__(self, switch, coordinator) -> None: 107 | """Initialize Light Switch device class.""" 108 | super().__init__(switch, coordinator) 109 | self.switch = switch 110 | 111 | 112 | class VeSyncSwitchEntity(VeSyncBaseEntity, SwitchEntity): 113 | """Representation of a switch for configuring a VeSync humidifier.""" 114 | 115 | def __init__(self, humidifier, coordinator) -> None: 116 | """Initialize the VeSync humidifier device.""" 117 | super().__init__(humidifier, coordinator) 118 | self.smarthumidifier = humidifier 119 | 120 | @property 121 | def entity_category(self): 122 | """Return the configuration entity category.""" 123 | return EntityCategory.CONFIG 124 | 125 | 126 | class VeSyncFanChildLockHA(VeSyncSwitchEntity): 127 | """Representation of the child lock switch.""" 128 | 129 | def __init__(self, lock, coordinator) -> None: 130 | """Initialize the VeSync outlet device.""" 131 | super().__init__(lock, coordinator) 132 | 133 | @property 134 | def unique_id(self): 135 | """Return the ID of this display.""" 136 | return f"{super().unique_id}-child-lock" 137 | 138 | @property 139 | def name(self): 140 | """Return the name of the entity.""" 141 | return f"{super().name} child lock" 142 | 143 | @property 144 | def is_on(self): 145 | """Return True if it is locked.""" 146 | return self.device.details["child_lock"] 147 | 148 | def turn_on(self, **kwargs): 149 | """Turn the lock on.""" 150 | self.device.child_lock_on() 151 | 152 | def turn_off(self, **kwargs): 153 | """Turn the lock off.""" 154 | self.device.child_lock_off() 155 | 156 | 157 | class VeSyncHumidifierDisplayHA(VeSyncSwitchEntity): 158 | """Representation of the child lock switch.""" 159 | 160 | def __init__(self, lock, coordinator) -> None: 161 | """Initialize the VeSync outlet device.""" 162 | super().__init__(lock, coordinator) 163 | 164 | @property 165 | def unique_id(self): 166 | """Return the ID of this display.""" 167 | return f"{super().unique_id}-display" 168 | 169 | @property 170 | def name(self): 171 | """Return the name of the entity.""" 172 | return f"{super().name} display" 173 | 174 | @property 175 | def is_on(self): 176 | """Return True if it is locked.""" 177 | return self.device.details["display"] 178 | 179 | def turn_on(self, **kwargs): 180 | """Turn the lock on.""" 181 | self.device.turn_on_display() 182 | 183 | def turn_off(self, **kwargs): 184 | """Turn the lock off.""" 185 | self.device.turn_off_display() 186 | 187 | 188 | class VeSyncHumidifierAutomaticStopHA(VeSyncSwitchEntity): 189 | """Representation of the automatic stop toggle on a VeSync humidifier.""" 190 | 191 | def __init__(self, automatic, coordinator) -> None: 192 | """Initialize the VeSync outlet device.""" 193 | super().__init__(automatic, coordinator) 194 | 195 | @property 196 | def unique_id(self): 197 | """Return the ID of this device.""" 198 | return f"{super().unique_id}-automatic-stop" 199 | 200 | @property 201 | def name(self): 202 | """Return the name of the device.""" 203 | return f"{super().name} automatic stop" 204 | 205 | @property 206 | def is_on(self): 207 | """Return True if automatic stop is on.""" 208 | return self.device.config["automatic_stop"] 209 | 210 | def turn_on(self, **kwargs): 211 | """Turn the automatic stop on.""" 212 | self.device.automatic_stop_on() 213 | 214 | def turn_off(self, **kwargs): 215 | """Turn the automatic stop off.""" 216 | self.device.automatic_stop_off() 217 | 218 | 219 | class VeSyncHumidifierAutoOnHA(VeSyncSwitchEntity): 220 | """Provide switch to turn off auto mode and set manual mist level 1 on a VeSync humidifier.""" 221 | 222 | def __init__(self, autooff, coordinator) -> None: 223 | """Initialize the VeSync outlet device.""" 224 | super().__init__(autooff, coordinator) 225 | 226 | @property 227 | def unique_id(self): 228 | """Return the ID of this device.""" 229 | return f"{super().unique_id}-auto-mode" 230 | 231 | @property 232 | def name(self): 233 | """Return the name of the device.""" 234 | return f"{super().name} auto mode" 235 | 236 | @property 237 | def is_on(self): 238 | """Return True if in auto mode.""" 239 | return self.device.details["mode"] == "auto" 240 | 241 | def turn_on(self, **kwargs): 242 | """Turn auto mode on.""" 243 | self.device.set_auto_mode() 244 | 245 | def turn_off(self, **kwargs): 246 | """Turn auto off by setting manual and mist level 1.""" 247 | self.device.set_manual_mode() 248 | self.device.set_mist_level(1) 249 | -------------------------------------------------------------------------------- /custom_components/vesync/translations/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "step": { 9 | "user": { 10 | "data": { 11 | "password": "\u041f\u0430\u0440\u043e\u043b\u0430", 12 | "username": "E-mail \u0430\u0434\u0440\u0435\u0441" 13 | }, 14 | "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." 10 | }, 11 | "error": { 12 | "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Contrasenya", 18 | "username": "Correu electr\u00f2nic" 19 | }, 20 | "title": "Introdueix el nom d'usuari i contrasenya" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." 10 | }, 11 | "error": { 12 | "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Heslo", 18 | "username": "E-mail" 19 | }, 20 | "title": "Zadejte u\u017eivatelsk\u00e9 jm\u00e9no a heslo" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "step": { 9 | "user": { 10 | "data": { 11 | "password": "Adgangskode", 12 | "username": "Emailadresse" 13 | }, 14 | "title": "Indtast brugernavn og adgangskode" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." 10 | }, 11 | "error": { 12 | "invalid_auth": "Ung\u00fcltige Authentifizierung" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Passwort", 18 | "username": "E-Mail" 19 | }, 20 | "title": "Benutzername und Passwort eingeben" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." 10 | }, 11 | "error": { 12 | "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", 18 | "username": "Email" 19 | }, 20 | "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}" 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Already configured. Only a single configuration possible." 10 | }, 11 | "error": { 12 | "invalid_auth": "Invalid authentication" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Password", 18 | "username": "Email" 19 | }, 20 | "title": "Enter Username and Password" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/es-419.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "step": { 9 | "user": { 10 | "data": { 11 | "password": "Contrase\u00f1a", 12 | "username": "Direcci\u00f3n de correo electr\u00f3nico" 13 | }, 14 | "title": "Ingrese nombre de usuario y contrase\u00f1a" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." 10 | }, 11 | "error": { 12 | "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Contrase\u00f1a", 18 | "username": "Correo electr\u00f3nico" 19 | }, 20 | "title": "Introduzca el nombre de usuario y la contrase\u00f1a" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." 10 | }, 11 | "error": { 12 | "invalid_auth": "Tuvastamise viga" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Salas\u00f5na", 18 | "username": "E-post" 19 | }, 20 | "title": "Sisesta kasutajanimi ja salas\u00f5na" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Changer le mode sur {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." 10 | }, 11 | "error": { 12 | "invalid_auth": "Authentification invalide" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Mot de passe", 18 | "username": "Email" 19 | }, 20 | "title": "Entrez vos identifiants" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." 10 | }, 11 | "error": { 12 | "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "\u05e1\u05d9\u05e1\u05de\u05d4", 18 | "username": "\u05d3\u05d5\u05d0\"\u05dc" 19 | }, 20 | "title": "\u05d4\u05d6\u05df \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05e1\u05d9\u05e1\u05de\u05d4" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." 10 | }, 11 | "error": { 12 | "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Jelsz\u00f3", 18 | "username": "E-mail" 19 | }, 20 | "title": "\u00cdrja be a felhaszn\u00e1l\u00f3nevet \u00e9s a jelsz\u00f3t" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." 10 | }, 11 | "error": { 12 | "invalid_auth": "Autentikasi tidak valid" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Kata Sandi", 18 | "username": "Email" 19 | }, 20 | "title": "Masukkan Nama Pengguna dan Kata Sandi" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." 10 | }, 11 | "error": { 12 | "invalid_auth": "Autenticazione non valida" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Password", 18 | "username": "Email" 19 | }, 20 | "title": "Immettere nome utente e password" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" 10 | }, 11 | "error": { 12 | "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", 18 | "username": "E\u30e1\u30fc\u30eb" 19 | }, 20 | "title": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." 10 | }, 11 | "error": { 12 | "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "\ube44\ubc00\ubc88\ud638", 18 | "username": "\uc774\uba54\uc77c" 19 | }, 20 | "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/lb.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." 10 | }, 11 | "error": { 12 | "invalid_auth": "Ong\u00eblteg Authentifikatioun" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Passwuert", 18 | "username": "E-Mail" 19 | }, 20 | "title": "Benotzernumm a Passwuert aginn" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/lv.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "step": { 9 | "user": { 10 | "data": { 11 | "password": "Parole", 12 | "username": "E-pasta adrese" 13 | } 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Al geconfigureerd. Slecht \u00e9\u00e9n configuratie mogelijk." 10 | }, 11 | "error": { 12 | "invalid_auth": "Ongeldige authenticatie" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Wachtwoord", 18 | "username": "E-mail" 19 | }, 20 | "title": "Voer gebruikersnaam en wachtwoord in" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/no.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." 10 | }, 11 | "error": { 12 | "invalid_auth": "Ugyldig godkjenning" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Passord", 18 | "username": "E-post" 19 | }, 20 | "title": "Fyll inn brukernavn og passord" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." 10 | }, 11 | "error": { 12 | "invalid_auth": "Niepoprawne uwierzytelnienie" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Has\u0142o", 18 | "username": "Adres e-mail" 19 | }, 20 | "title": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o." 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "error": { 9 | "invalid_auth": "Autentica\u00e7\u00e3o invalida" 10 | }, 11 | "step": { 12 | "user": { 13 | "title": "Digite o nome de usu\u00e1rio e a senha" 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." 10 | }, 11 | "error": { 12 | "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Palavra-passe", 18 | "username": "Endere\u00e7o de email" 19 | }, 20 | "title": "Introduza o nome de utilizador e a palavra-passe" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." 10 | }, 11 | "error": { 12 | "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "\u041f\u0430\u0440\u043e\u043b\u044c", 18 | "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" 19 | }, 20 | "title": "VeSync" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "error": { 9 | "invalid_auth": "Neplatn\u00e9 overenie" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "username": "Email" 15 | } 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "step": { 9 | "user": { 10 | "data": { 11 | "password": "Geslo", 12 | "username": "E-po\u0161tni naslov" 13 | }, 14 | "title": "Vnesite uporabni\u0161ko Ime in Geslo" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "step": { 9 | "user": { 10 | "data": { 11 | "password": "L\u00f6senord", 12 | "username": "E-postadress" 13 | }, 14 | "title": "Ange anv\u00e4ndarnamn och l\u00f6senord" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." 10 | }, 11 | "error": { 12 | "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "Parola", 18 | "username": "E-posta" 19 | }, 20 | "title": "Kullan\u0131c\u0131 Ad\u0131 ve \u015eifre Girin" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." 10 | }, 11 | "error": { 12 | "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "\u041f\u0430\u0440\u043e\u043b\u044c", 18 | "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" 19 | }, 20 | "title": "VeSync" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "error": { 9 | "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "password": "\u5bc6\u7801", 15 | "username": "\u7535\u5b50\u90ae\u4ef6" 16 | }, 17 | "title": "\u8f93\u5165\u7528\u6237\u540d\u548c\u5bc6\u7801" 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /custom_components/vesync/translations/zh-Hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "device_automation": { 3 | "action_type": { 4 | "set_mode": "Change mode on {entity_name}." 5 | } 6 | }, 7 | "config": { 8 | "abort": { 9 | "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" 10 | }, 11 | "error": { 12 | "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" 13 | }, 14 | "step": { 15 | "user": { 16 | "data": { 17 | "password": "\u5bc6\u78bc", 18 | "username": "\u96fb\u5b50\u90f5\u4ef6" 19 | }, 20 | "title": "\u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Custom VeSync", 3 | "render_readme": true 4 | } 5 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2024 micahqcade 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyvesync==2.1.12 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | homeassistant==2022.7.7 3 | black 4 | isort 5 | flake8 6 | pre-commit 7 | setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 3 | max-complexity = 25 4 | doctests = True 5 | # To work with Black 6 | # E501: line too long 7 | # W503: Line break occurred before a binary operator 8 | # E203: Whitespace before ':' 9 | # D202 No blank lines allowed after function docstring 10 | # W504 line break after binary operator 11 | ignore = 12 | E501, 13 | W503, 14 | E203, 15 | D202, 16 | W504 17 | noqa-require-code = True 18 | --------------------------------------------------------------------------------