├── .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 | [](https://github.com/custom-components/hacs)
7 | [](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 | [](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 | [](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 | 
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 |
--------------------------------------------------------------------------------